OpenMuse 这类接入最容易失控的地方,不是模型答得对不对,而是工具注册之后默认“凡是注册过就能被调用”。一旦某个写文件、发请求、执行命令的函数混进注册表,模型在一次自动多轮里就可能把它触发,而且触发过程未必有人看见。处理方向很直接:接入后先把可被调用的工具收敛成一份显式白名单,默认拒绝未登记项;再用一次带副作用的测试确认未登记工具确实不会执行;最后把每次拦截写进日志,让“没执行”这件事可追溯,而不是靠印象判断。
判断方向:接入完成后,先把工具注册表当成暴露面清单完整列出来,再按副作用筛选出允许模型直接触发的子集,其余一律默认拒绝。操作上,白名单配置要放在工具注册之后、会话开始之前加载,保证每次调用都过一遍策略。验证方式是用一个明确指向未登记工具的提问,检查日志中出现拒绝记录且对应副作用没有发生。边界:白名单只能约束走注册入口的调用,绕过注册、直接调用函数代码的路径需要另外确认。
列出项目里可能被调用的工具函数
这一步的目标是把暴露面摸清楚,而不是急着做筛选。只要一个函数以某种形式交给了模型侧的工具描述,它就属于候选范围。先找到项目里注册工具的位置,通常是一个装饰器、一个列表常量,或者一个 schema 定义文件。可以用关键词搜索把候选抓出来,命令里的模式按你项目实际的注册写法替换:
# 在项目根目录执行,按实际注册入口名替换关键词
grep -rn "register_tool\|tool_schema\|tools=" `--include`="*.py" .
把命中的函数逐个登记到下面这张表里。关键是“被谁引用”这一列要写清楚:是直接被塞进工具列表,还是经由某个工厂函数拼装进去——后者容易被漏掉,因为它不在搜索结果的第一层。
- 函数名:注册或暴露时使用的名字,注意是否和代码里的函数名不一致
- 所在文件:具体到文件路径,便于后续改动时对照
- 被谁引用:注册入口、工厂函数、配置文件或动态加载逻辑
清单做完先不删任何东西。此时它是一份事实记录,能回答“接入后到底有哪些能力可能被触发”这个问题;找不到限制入口,往往就是因为这一步从来没做过。
给每个工具标出输入输出与副作用
把上一步的清单换成判断依据:哪些工具可以让模型直接触发,哪些必须挡在白名单之外。判断的最小信息是参数类型、返回内容,以及是否写数据或发请求。参数里出现文件路径、URL、shell 命令、SQL 片段这类字段的,风险等级天然更高,因为模型的输入会直接拼进副作用操作。
- 参数类型:字符串、数字、枚举,还是自由文本;自由文本更容易被间接注入
- 返回内容:返回摘要还是返回完整内容,完整内容会继续进入上下文
- 副作用:是否写文件、写库、发外部请求、修改远端状态、执行本地命令
一个可以先用起来的切分方式是三档:只读且无外部请求的,通常可以先放行;有写入但可回滚、参数受枚举约束的,建议加参数校验再放行;会发外部请求或执行命令的,建议默认不放进白名单,需要时再单独评估。这里没有统一答案,需要结合你的部署环境确认——比如同一份代码在本地和线上,副作用的影响范围并不一样。
写一份白名单配置骨架
下面是一份通用结构,字段名只是示例,替换成你项目里加载策略的那层实际读取的名字即可,不要照抄成某个虚构接口。
tool_policy:
mode: allowlist # allowlist:只有 allow 列表里的名字可调用
default: deny # 未命中任何规则时的默认结果
allow:
- name: search_notes
args:
max_results: { type: int, max: 20 }
- name: read_file
args:
path: { type: str, pattern: "^/data/docs/" }
deny:
- write_file
- run_shell
- http_fetch
登记与未登记的差别要能被策略层明确表达:登记项通过校验后进入执行,未登记项在进入执行前就被拒绝,且拒绝结果要能被调用方和日志同时看到。加载位置建议放在工具注册完成之后、会话开始之前——注册之后能拿到完整名单做校验,会话之前能保证第一次调用就已经受约束。如果策略是热加载的,还要确认加载失败时的行为,宁可按拒绝处理,也不要退回到“全部允许”。
验证未登记的工具确实不被执行
白名单写完不等于生效,要用行为确认。挑一个明确指向未登记工具的提问,比如让模型写文件或抓取某个地址,看它是否真的做不到。
- 测试提问:设计一个只有调用未登记工具才能完成的任务,例如“把这段内容写进 /tmp/out.txt”。
- 期望现象:模型给出无法完成、工具不可用或改走其他方式的回答,而不是直接生成成功结果。
- 判断标准:同时满足两条——日志里出现针对该工具的拒绝记录;对应的副作用没有发生,比如目标文件未被创建、外部请求没有发出。
只看回答文字容易被误导,模型有时会顺着说“已完成”。所以判断标准要落在日志记录和实际状态检查上。如果日志没有拒绝记录、但副作用也没发生,那说明调用根本没被触发,这一轮验证是无效的,需要换一个更明确的提问重试。
记录被拦截的调用
拦截只有被记录才可追溯。日志字段建议至少包含以下几项,名字可以按你现有的日志规范调整:
- 时间戳与会话标识:用来把同一次多轮调用串起来
- 工具名与参数摘要:参数做截断或脱敏,避免把敏感内容整段写进日志
- 决策结果:allow 或 deny
- 决策原因:未登记、参数校验失败、命中 deny 规则
- 策略版本或配置哈希:方便回溯是哪一版白名单做出的判断
拦截和执行失败要区分开。决策结果是 deny,说明请求在策略层就被挡住,工具函数没有被调用;如果出现的是执行阶段的报错,比如超时、权限不足、参数在运行时才失败,那是已经进入执行层之后的事。两者混在同一个字段里,后续排查会分不清是白名单起作用了,还是工具本身出了问题。分开记录之后,前面那轮验证的“判断标准”也才有据可查。