3663 字
约 12 分钟
15
2.5 为什么模型本身不能「动手」
2026-10-25
无标签

2.5 为什么模型本身不能「动手」

一句话结论:模型能输出一切"能被文本表示"的东西——包括一个工具调用请求——但它永远不执行任何东西。"动手"这个能力,从头到尾都在你的代码里。 认清这一点,Agent 的安全、可控、可审计才有了落脚点。

读这篇之前

需要先读 1.5(模型只输出 tool_calls)和 2.1(模型在预测 Token)。这一篇是第 2 章的收束,把"模型边界"讲到底。


一、把话说到底:模型的输出是什么

模型的输出,本质上就是一串 Token。这串 Token 可以被解释成各种东西:

你让它输出 它实际给了你 谁来解释
一段话 文字 人眼
一段 JSON 符合 JSON 形状的文字 你的解析器
一个工具调用 tool_calls 结构化字段 你的框架代码
一段代码 代码文本 你的执行环境
一条 SQL SQL 文本 你的数据库客户端

注意最后三行:模型交付的是"文本",执行是后面那一列的事。

所以当有人说"模型把生产库删了"时,准确的说法是:模型输出了那条语句,而某个没有做校验的程序把它交给了数据库执行。

这个区分不是文字游戏,它决定了责任归属和防护位置:

  • 如果风险在模型 → 你要换模型、加提示词
  • 如果风险在执行层 → 你要在执行层加校验、加白名单、加人工确认

真正的风险在执行层。 这就是为什么第 14 章整章都在讲权限与人在环。


二、为什么"让模型直接动手"在设计上不可行

有人会问:既然模型能生成代码,为什么不让它直接执行?

原因有三层,从根本到表面:

第一层:它没有执行环境

模型是一个函数:给 Token,返回 Token。它没有文件系统、没有网络、没有进程。

要让它"动手",必须有外部程序代为执行——这就是工具。

第二层:它的输出不具备"可信"属性

即使你让它生成一段脚本并执行,这段脚本的正确性没有任何保证:

  • 它可能基于错误的前提(2.3 幻觉)
  • 它可能用了不存在的参数(具体细节最易编)
  • 它可能在小概率下生成了完全不同的内容(2.4 采样)

让一个概率生成器直接驱动不可逆操作,在工程上不可接受。

第三层:它无法感知执行的后果

模型不能"看到"执行的结果,除非你把结果放进上下文再调用一次(1.3)。

所以即使让它执行了,它也不知道执行对不对——中间必须有一个能判断、能回滚、能拦截的层。


三、工具层应该承担什么

既然执行在我们手里,那么工具层就不是"简单的转发",而是一道必须做判断的关卡。

一个负责任的工具层至少要做五件事:

职责 具体做法 不做会怎样
参数校验 类型、范围、必填项、格式 模型传错参数导致误操作
权限检查 这个任务允许调这个工具吗 越权访问
副作用分级 读操作直接放行,写操作走审批 不可逆损害
结果归一化 排序固定、剔除噪声字段 上下文不稳定(2.4)
失败封装 把异常转成模型能理解的错误信息 模型无法自我纠正

最后一条常被忽视,但它直接决定 Agent 的"纠错能力":

❌ 把异常原样抛出:Error: ECONNREFUSED 127.0.0.1:5432
✅ 转成模型能用的信息:数据库连接失败(服务未启动或端口不通),建议先确认数据库是否运行

错误信息的质量,决定了模型能不能自我修复。 这本质上也是上下文工程(2.2)的一部分。


四、一个完整的例子:把"看天气"拆到最底层

把整个过程拆到你能看清每一层的责任:

① 用户:"明天北京天气怎么样?"
② 你的代码拼装上下文(system + user)→ 发给模型
③ 模型返回:{"tool_calls":[{"name":"get_weather","arguments":{"city":"北京","date":"明天"}}]}
        └─ 模型的工作到此结束
④ 你的代码:
   a. 校验参数(city 存在?date 格式对?)
   b. 检查权限(当前任务允许调 get_weather?)
   c. 分级(读操作,直接执行)
   d. 真的发起 HTTP 请求
   e. 归一化结果(去掉时间戳、固定字段顺序)
⑤ 你的代码把结果追加为一条 tool 消息
⑥ 你的代码再次调用模型 → 模型生成自然语言回答
⑦ 你的代码把回答返回给用户

数一下:模型只参与了第 ③ 步和第 ⑥ 步。其余六步全在你的代码里。

这就是 Agent 的真实形态——它是你的程序在驱动模型。模型只是其中最聪明的一个零件。


五、这个认知带来的三个实际好处

好处一:安全边界画得清

不要再问"怎么让模型不干坏事",而要问:

我的工具层允许模型做什么?

把工具清单、参数白名单、审批规则列出来,风险就可控。模型在这个边界内怎么"自由发挥"都不会造成损害。

好处二:调试方向明确

Agent 出错时,按这个顺序查:

① 上下文对不对?(缺信息?混进错信息?)
② 模型判断对不对?(换更强模型试)
③ 工具实现对不对?(单独测工具)
④ 参数校验够不够?(看传进来的参数)

绝大多数问题在 ① 和 ③,而不是 ②。

好处三:可以渐进增强

因为模型只负责"判断",你可以逐步增强外围而不用改模型:

  • 加检索 → 模型知道更多
  • 加记忆 → 模型记得更久
  • 加状态栏 → 模型数得更准
  • 加校验 → 模型出错的后果更小

每一步都是外围工程,风险低、可回滚。


六、第 2 章小结

五篇讲完,把模型这一侧的全部约束压缩成一张表:

约束 本质 工程应对 详见
Token 计量 按 Token 计费,中文密度更高 估算成本、压缩中文 2.1
上下文窗口 输入+输出有硬上限;检索强、提炼弱 分层装、状态外显、压缩 2.2
幻觉 优化"像不像"而非"真不真" 给材料、要引用、允许说不知道 2.3
采样随机 从概率分布抽样 输出类环节降温度;查输入噪声 2.4
不能执行 只输出文本,执行在外围 工具层做校验、分级、归一化 2.5

三条最重要的结论:

  1. 模型是零件,不是系统。 决定 Agent 可靠性的是外围工程。
  2. 上下文是天花板。 提效果先补上下文,再考虑换模型。
  3. 风险在执行层。 防线的位置在工具与权限,不在提示词。


七、工具设计上的五个具体判据

上一节讲了工具层"该承担什么",这一节把它落成可检查的判据。每个都来自真实系统的教训:

# 判据 反例 后果
1 一个工具做一件事 manage_user(action, data) 一个工具干增删改 参数组合爆炸,模型容易传错
2 参数尽量用枚举,不要用自由文本 path: "随便填个路径" 越权访问任意文件
3 读与写必须分成两个工具 query(sql) 里允许 UPDATE 无法单独对写操作加审批
4 返回值要小且稳定 直接返回 500KB 的 JSON 撑爆上下文、顺序不稳
5 错误信息要可行动 抛裸异常 模型无法自我纠正

第 2 条和第 3 条尤其重要,它们直接决定第 14 章的权限模型能不能落地:

  • 枚举参数让"允许的范围"可以被静态定义
  • 读写分离让"不可逆操作"可以被单独拦截

一个常见的错误设计

❌ exec_command(cmd: string)

这一个工具等于把整个 shell 交给模型。它极其灵活,也极其危险——你无法对它做参数校验,也无法分级副作用。

更稳的做法是拆成一组合法的具体动作:

✅ read_file(path)        // 读,白名单目录内
✅ write_file(path, text) // 写,需审批
✅ run_test(name)         // 跑测试,固定白名单
✅ git_diff()             // 只读状态

这看起来限制了能力,实际上反而让 Agent 更可靠——因为每个动作的边界清晰,模型不容易用错,你也容易拦截。

第 9 章要讲的 Skills、第 4.5 节的「工具爆炸」,本质都是这个思路的延伸:用更少的、更明确的接口,替代更少的约束。


八、常见误区补充

误区六:工具越多,Agent 越强。
工具数量增加会挤占上下文(2.2)、增加选择错误概率。克制是工具设计的第一原则(详见 4.5)。

误区七:模型输出的 JSON 一定合法。
不一定。尤其在长输出、嵌套结构、含特殊字符的场景下,它会漏括号、多逗号。必须有 schema 校验与重试机制(详见 3.2)。

误区八:工具报错就等于 Agent 失败。
恰恰相反,结构化、可读的错误信息是 Agent 自我纠错的关键输入。报错要报得"有营养"。


九、动手:一个最小工具层的完整实现

把"工具层要做五件事"落到实处。

def execute_tool(call, registry, policy):
    # ① 工具存在?
    tool = registry.get(call.name)
    if not tool:
        return tool_error(f"未知工具:{call.name}。可用工具:{list(registry)}")

    # ② 参数校验(结构)
    missing = [k for k in tool.required if k not in call.args]
    if missing:
        return tool_error(f"缺少必填参数:{missing}")
    for k, spec in tool.schema.items():
        if k in call.args and not check_type(call.args[k], spec.type):
            return tool_error(f"参数 {k} 应为 {spec.type}")

    # ③ 语义校验(枚举/范围/白名单)
    err = tool.semantic_check(call.args)
    if err:
        return tool_error(err)

    # ④ 权限与副作用分级
    if not policy.allows(tool.name):
        return tool_error(f"当前任务不允许调用 {tool.name}")
    if tool.side_effect == "irreversible" and not call.approved:
        return tool_need_approval(tool.name, call.args)

    # ⑤ 执行 + 归一化
    try:
        raw = tool.run(**call.args)
        return tool_ok(normalize(raw, tool.output_schema))
    except Exception as e:
        return tool_error(tool.explain_error(e))

五个返回类型很关键

返回 模型的反应
tool_ok 继续
tool_error(可纠正) 修正参数重试
tool_need_approval 停下来等人
tool_denied 换方案或放弃
tool_unavailable 跳过这个工具

区分这五种,比统一返回"错误"有用得多——因为它们对应完全不同的模型行为。

最常见的三个实现漏洞

漏洞 后果
只在工具内部校验,不在调度层校验 校验被绕过
normalize 没做(直接返回原始 JSON) 上下文膨胀 + 顺序不稳
explain_error 直接返回异常文本 模型无法自我纠正

这三个漏洞,本质都是"把该在工具层做的事推给了模型"。


十、把"模型不能动手"变成架构约束

理解这一点,最实际的用法是在架构上把它固化。

三条可写进设计文档的约束

① 任何"执行"动作必须经过工具层,禁止任何"直接解析模型输出后执行"的捷径。
② 工具层必须独立于模型(可单测、可 mock、可在无模型情况下跑)。
③ 不可逆动作必须有第三道闸(工具层校验 + 审批 + 幂等)。

第 ② 条常被忽略但极其重要:如果工具层无法脱离模型单独测试,你就永远不知道"出错是模型选错了,还是工具实现错了"。

一个反例

# 错:把模型输出直接丢给 eval
result = eval(model_response)

这是最危险的写法,因为它把"概率生成器"直接接到"任意代码执行"。

正确写法是只接受枚举的动作:

allowed = {"read": do_read, "write": do_write, "search": do_search}
action = model_response.get("action")
if action not in allowed:
    return error("不支持的动作")
return allowed[action](**model_response["args"])

这两段代码的区别,就是"Agent 安全"与"事故"的区别。


十一、第 2 章小结(模型边界部分)

五篇的核心可以压成一句:

模型是一个"读上下文、写文本"的纯函数;所有能力靠外围工程补。

约束 应对
只预测 Token 补材料(RAG)、要引用
上下文有限 分层装、状态外显、压缩
会幻觉 给证据、允许说不知道、外部校验
采样随机 降温度、修顺序与噪声
不能执行 工具层校验 + 权限 + 审批

这五条构成了后面所有章节的地基。


十二、下一篇

第 2 章讲的是"模型的边界"。从第 3 章开始,我们正式进入"怎么在这个边界内把事做成"。

第一个要讲的,是与模型沟通的最基本手段——提示词。它不是"说话的艺术",而是一套有明确机制、能预测失败模式的工程方法。

2.5 为什么模型本身不能「动手」
https://clxhxhhr.top/posts/4876/
作者
clxstart
发布于
2026-10-25
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。