Agent 说「我已经改好文件了」,这句话到底能不能信?

49 阅读5分钟

用 AI 编程工具的人多半见过这个场面:它一路说得很笃定——「已定位到问题」「已修改 shipping.ts」「测试已通过」——你一看文件,一个字没动。

第一反应通常是「这模型在胡说」。

但读完六套编程智能体的源码(Codex、Claude Agent SDK、Gemini CLI、DeepSeek Harness、pi、OpenCode,笔记整理成了一套开源中文教材,文末有链接)之后我发现,这个判断不太对。模型不是在撒谎,它根本不知道文件有没有被改

因为改文件这件事,压根不是它干的。

1. 三个角色,别混成一个

读 Agent 源码最容易犯的错,就是把所有行为都算到模型头上。实际上一次任务至少牵扯三方:

Model        产生候选决定 —— 只是「建议这么做」
  ↓
Harness      把候选接进任务循环 —— 检查、放行、调用执行器
  ↓
Environment  副作用真正发生的地方 —— 文件、进程、网络

模型输出的从来不是动作,是动作的请求。简化成教学示例大概是这样:

{
  "type": "tool_call",
  "name": "read_file",
  "arguments": {"path": "src/shipping.ts"}
}

这一刻文件还没打开。模型不知道宿主进程有没有读权限,也不知道产品策略会不会放行这个路径。它只是提了个请求。

所以当它说「我读了文件」,准确的说法是「我请求读文件,并且假设那一步成功了」。

2. Harness 在中间做了六件事

夹在中间的这一层通常要负责:

  1. 把系统提示、用户消息、历史、工具定义、动态状态拼成模型输入
  2. 发起模型请求,消费流式或非流式响应
  3. 区分文本完成、工具请求、错误、取消、超时
  4. 检查工具名、参数、权限、用户确认
  5. 调执行器,把结果写回消息历史
  6. 保存 Session、发事件,决定继续还是结束

六套实现切法完全不同。Codex 把核心循环、协议和多种界面分到不同 crate;OpenCode 让多个客户端连到服务化的 Session;Claude 的 Agent SDK 只公开应用进程能看到的部分,产品内部怎么做我们看不到。职责看着相似,模块名字对不上。

回到那个运费 bug:Harness 拿到 read_file 请求后,先交给工具注册表,检查路径在不在工作区内,然后才真的去读。读到的内容回写进下一轮:

{
  "type": "tool_result",
  "tool_call_id": "call-1",
  "content": "export function shippingFee(total) { return total > 100 ? 0 : 10 }"
}

模型看到这个结果,才知道边界条件确实写错了,才有依据提出把 > 改成 >=

在这之前它说的任何「我看到了」,都只是它以为。

3. Environment 才是说了算的那层

策略放行了 src/shipping.ts,文件仍然可能打不开:文件不存在、宿主进程没权限、Sandbox 没挂载那个目录、远程工作区断线。

这些都是执行失败。把它们记成「策略拒绝」,账就算错了——你会以为是权限配置问题,实际是环境问题,排查方向全反。

反过来也一样:宿主进程也许有能力删掉整个仓库,但 Harness 不该因此就批准删除。系统能做什么,和产品授权它做什么,是两笔账。

4. 一张排查表

出问题时先判断落在哪一层,能省很多时间:

现象更可能是哪一层去查什么
模型压根没请求任何工具Model 或输入构造模型请求、工具定义、上下文
工具请求被判为未知Harness工具注册和名称解析
请求被规则拒绝Harness权限策略、确认结果
请求获准但文件打不开Environment路径、挂载、系统权限
测试退出码 0Environment 事实再查测试有没有覆盖用户目标

最后一行值得单说。

5. 「测试通过了」也不等于「做完了」

退出码 0 要成为有效证据,得同时满足三件事:

  • 它覆盖了用户目标
  • 跑的是修改后的代码
  • 输出确实来自这次任务

三条缺一条,这个 0 就说明不了问题。举个具体的:如果测试只跑了 shippingFee(101),退出码是 0,但金额 100 的那个 bug 一点没修——而那正是用户要解决的问题。

进程状态和任务结果是两回事。前者是 Environment 的事实,后者需要按明确口径去判断。

6. 问题考察

下面四句话,分别是谁作出的判断?

  1. 「下一步应当把 > 改成 >=。」
  2. 「本次写入需要用户确认。」
  3. shipping.test.ts 返回退出码 0。」
  4. 「边界值测试通过,且测试文件未被修改,因此任务结果合格。」

依次是:Model 提出的候选做法、Harness 按策略作出的决定、Environment 里真实发生的执行结果、以及对整个结果的独立判定。

第三句推不出第四句——你还得确认那个退出码对应的是正确的命令、正确的工作区、以及这一次的补丁。

7. 所以回到开头那个问题

Agent 说「我已经改好文件了」,能不能信?

不能,但也不该怪它。 它说的是「我提了这个请求」,中间隔着两层它看不见的东西。想确认到底改没改,去看 Environment:文件内容、Diff、退出码。

这也是为什么读 Agent 源码时,第一件该做的事不是找「智能」在哪,而是先把这三层的边界画出来。边界画清楚了,你才知道一个 bug 该去哪一层排查。

8. 全量笔记

这篇讲的是三层边界,是理解 Agent Harness 的地基。完整教材还包括:配置和 Prompt 怎么拼成模型看到的输入、工具循环怎么转、权限在哪几层拦、Turn 和 Step 的边界在哪、会话状态怎么存和恢复、多个子 Agent 怎么协作——六套实现每套一条课程,外加横向对比。

免费开源,觉得有用点个 star,写错的地方欢迎提 Issue。