AI 编码 Agent 从原理到可运行代码

1. 概述

过去五年,开发者与 AI 的关系被「Tab 键」定义:模型猜下一行,人决定接不接受。GitHub Copilot 把这件事做到了极致,也把很多人锁在一个错误心智模型里------以为 AI 写代码的上限,就是更准的补全、更长的提示词、更大的上下文窗口。

2024 到 2026 年真正发生的变化,不是模型突然「更会写代码」,而是产品形态从生成器 变成了执行器。Claude Code、Cursor Agent、Codex CLI、Devin、Aider、Cline 这些工具表面长得不一样:有的住在终端,有的住在 IDE,有的住在云端虚拟机。底层却收敛到同一件事:

给大模型一双手(工具),再给它一个不停转的循环(观察 → 推理 → 行动 → 再观察),让它在真实代码仓库里自己把任务做完。

这个循环有一个学术名字:ReAct(Reason + Act)。2022 年提出时,它只是让 LLM 交替输出「思考」和「动作」。2026 年,它已经变成几乎所有编码 Agent 的操作系统内核。模型本身仍然是无状态的:每一次 API 调用都看不到上一轮之外的世界。真正让 Agent「像工程师一样干活」的,是循环外面那一层工程系统------上下文怎么拼、工具怎么派发、编辑怎么落地、失败怎么回收、权限怎么卡死。

这篇文章面向两类读者。一类是刚接触 Agent 的开发者:你不需要先啃完论文,也能顺着比喻和流程图把原理看懂。另一类是准备自己做、或准备把现有工具用到极限的工程师:你会看到生产级架构怎么分层、主流产品怎么取舍、以及一份可以直接跑的实现。

读完你应该能回答五个问题:

  1. 编码 Agent 和 Copilot、ChatGPT 写代码,差在哪一层?
  2. 那个「会自己干活」的循环,到底在转什么?
  3. 为什么同样一个模型,套上不同 Harness(驾驭层)表现天差地别?
  4. 自己从零写一个能改文件、跑命令、根据测试结果自我修复的 Agent,最少要哪些代码?
  5. 接下来两年,开发者的工作会变成编排、审核和定标准,而不是和 Tab 键较劲。

一句话先行结论:模型是大脑,工具是手,循环是意志,上下文工程是视力,安全沙箱是良心。缺任何一块,你得到的都只是会说话的补全,而不是能交付的工程师。


2. 内容

简要介绍:这一节先把概念立住,再把原理拆开。先用三个日常场景区分「补全 / 聊天写代码 / 编码 Agent」,再把 ReAct 循环、五件套(规划、记忆、工具、执行、反思)、上下文工程、Function Calling 与 CodeAct、以及代码编辑策略一层层摊开。读这一节时,请始终记住一个事实:大模型不会「记住」仓库,也不会「执行」代码;它只是在每一轮里,根据你塞给它的 token,决定下一句话或下一次工具调用。Agent 的全部魔法,都发生在这一轮和下一轮之间。

2.1 从 Copilot 到 Agent:一次被低估的范式跃迁

先用一个具体任务把三种形态钉死。假设你接到一张工单:

「用户登录接口在 address 为空时会 NPE。补上空值保护,补回归测试,确认 mvn test 全绿。」

形态 A:行内补全(Copilot Tab / Cursor Tab)。

你打开 UserController.java,光标停在 address.getStreet() 前面。模型猜出 if (address == null) return;。你按 Tab。测试还是你自己写,构建还是你自己跑,相邻的 DTO 和异常处理还是你自己找。它加速的是击键 ,不加速任务闭环

形态 B:聊天生成(ChatGPT / Claude 对话框)。

你把文件贴进去,模型吐出一段修好的代码。你复制、粘贴、跑测试、发现测试没覆盖空地址、再回去问模型、再复制。模型能推理,但没有手。仓库、测试、报错,全靠你当中间人。

形态 C:编码 Agent。

你只给一句话目标。Agent 自己 grep 空指针调用点,读相关测试,改实现,跑 mvn test,看到 3 个用例过了但缺空地址用例,再补测试,再跑,4 个全绿,最后给出 diff 或直接开 PR。中间可能要 8 到 30 轮工具调用。你审核的是结果,不是每一行击键。

三种形态可以画成一条能力阶梯:
flowchart LR A"补全\
下一行 token"
--> B"聊天生成\
一段代码"
B --> C"单轮工具调用\
读一个文件"
C --> D"Agent 循环\
改-测-修直到完成"
D --> E"多 Agent / 云端\
并行子任务 + PR"

2021 到 2026,业界其实走的就是这条阶梯,只是每次换名字:

阶段 大致时间 核心问题 工程对象 失败形态
代码补全 2021--2022 下一行对不对 模型 + 局部上下文 建议不准
提示词工程 2022--2023 这一轮有没有说清楚 指令措辞、角色、示例 模型听错约束
上下文工程 2024--2025 模型有没有看到该看的东西 检索、仓库地图、历史压缩 自信地改错地方
Harness 工程 2025--2026 多步之后还能不能做对 工具、权限、验证器、循环 循环空转或改崩仓库
工作流工程 2026 起 这件事能不能周期性自动完成 目标、调度、验收标准 无人值守后质量漂移

对初学者最重要的分界线是这一句:

Chatbot 回答问题;Agent 追求目标。

Chatbot 的停止条件是「模型说完了」。Agent 的停止条件是「环境证明目标达成了」------测试绿了、lint 干净了、PR 描述写好了,或者触发了最大步数 / 人工叫停。

所以,编码 Agent 不是「更聪明的 Copilot」。Copilot 优化的是 token 预测;Agent 优化的是在有副作用的环境里,把一个软件工程目标收敛到可验证的完成态。前者是语言模型问题,后者是控制系统问题。大脑可以共用,控制系统不能省。

再补一个容易混的词:Harness(驾驭层)。2026 年这个词被用滥了,但意思很具体:模型之外、让模型能在仓库里安全地连续行动的那一层软件。Claude Code、Cursor Composer、Aider、Codex CLI 卖的主要不是模型(很多还允许换模型),而是 Harness:循环怎么转、上下文怎么拼、工具白名单、编辑格式、git 回滚、子 Agent、Hooks。同一颗 Claude Opus,塞进「只读聊天框」和塞进「带测试闭环的终端 Agent」,SWE-bench 分数可以差出一个时代。

2.2 ReAct 循环:编码 Agent 的心脏

ReAct 来自 Yao 等人 2022 年的论文 ReAct: Synergizing Reasoning and Acting in Language Models。想法朴素到近乎无礼:不要让模型一次性空想出最终答案,而让它像人一样,边想边做边看。

三拍循环:

text 复制代码
Thought(思考) → Action(行动) → Observation(观察)
        ↑                                    │
        └────────────────────────────────────┘
                     直到任务完成

翻译成编码场景:

text 复制代码
观察:  UserController.java 第 47 行,address.getStreet() 没有空判断
规划:  在调用前加 null check
行动:  写入修复
观察:  mvn test ------ 3 通过,0 失败
规划:  还要确认空地址路径有没有测试
行动:  读 UserControllerTest.java,发现没有 null 用例
规划:  补回归测试
行动:  写入测试
观察:  mvn test ------ 4 通过,0 失败
结束:  可以开 PR

用流程图画生产级循环,会比论文里的三拍多两步,因为工程上必须处理「拼上下文」和「要不要停」:
flowchart TD Start("收到任务") --> Assemble"拼装上下文\
系统提示 / 仓库摘录 / 历史 / 工具结果"
Assemble --> LLM"调用 LLM" LLM --> Decide{"返回了什么?"} Decide -->|"纯文本,无工具调用"| Done("回复用户,结束本轮任务") Decide -->|"一个或多个工具调用"| Dispatch"工具分发与安全校验" Dispatch --> Exec"执行:读文件 / 写文件 / grep / bash / 测例" Exec --> Observe"把 stdout、stderr、退出码、diff 变成 Observation" Observe --> Append"追加到对话历史" Append --> Limit{"达到最大步数<br/>或用户中断?"} Limit -->|否| Assemble Limit -->|是| Stop("安全停止并汇报现状")

请务必把下面这句话刻进脑子:模型是无状态的。 所谓「Agent 记得刚才读过 UserController.java」,并不是模型内部有一块内存芯片,而是编排器把那次 read_file 的返回值,原样(或压缩后)塞进了下一轮 messages。Agent 的「记忆」,在最小实现里就是一个不断变长的 messages 数组。

伪代码几乎短到尴尬------所有生产系统都是在这六行外面堆工程:

python 复制代码
while not done and steps < MAX_STEPS:
    context = assemble(system_prompt, history, tool_results)
    response = llm.complete(context, tools=TOOLS)
    if not response.tool_calls:
        return response.text          # 模型认为做完了
    results = [dispatch(call) for call in response.tool_calls]
    history.append(response, results) # 观察写回
    steps += 1

初学者常有三个误判,这里提前拆掉:

误判 1:循环越长越聪明。

不是。每多一轮就多一次推理误差、多一截噪声上下文、多一笔 token 账单。优秀 Harness 的目标是用更少的高信号步骤完成任务,而不是放任模型在仓库里散步。Sourcegraph 在 2026 年的 CodeScaleBench 里给过一个刺眼对比:同样的跨文件重构,基线 Agent 用本地 grep 走了 96 次工具调用、84 分钟;换成代码图谱检索后只需 5 次调用、4.4 分钟,奖励分数还翻倍。循环次数是成本,不是能力。

误判 2:Thought 必须用自然语言写出来。

早期 ReAct 示范里,模型会先说「我应该先读测试文件」。现代 Function Calling 里,Thought 常常折叠进模型内部的 chain-of-thought / extended thinking,对外只暴露结构化的 tool_calls。对编排器来说,工具调用就是行动,工具结果就是观察。你在 Claude Code 终端里看到的「正在读取 xxx」,是产品把内部状态翻译给你看,不是循环本身需要向你播报。

误判 3:模型会在循环中途「学会」新技能。

单次任务循环里的学习,只是上下文里多了几条 Observation。关掉会话,权重不会变。真正跨会话的改进,要靠后文要讲的记忆文件、经验库、评测回流,而不是幻想「它用过一次 pytest 就永远更懂 pytest」。

一次真实任务通常 5 到 50 轮。每轮至少一次 LLM 调用。这就是为什么编码 Agent 比聊天贵,也是为什么上下文管理会成为 2026 年最关键的工程问题:你不是在付「写一段代码」的钱,你是在付「一个初级工程师坐在你电脑前面,连续读报错、改文件、再跑 20 分钟」的钱。

2.3 五件套:规划、记忆、工具、执行、反思

ReAct 只是骨架。骨架上不焊部件,Agent 走不远。业界把这些部件叫法不一,但功能稳定,可以记成五件套:

组件 它在循环里干什么 没有它会怎样 编码场景里的典型形态
Planning 规划 把「修登录 NPE」拆成可执行步骤 走一步看一步,复杂重构必跑偏 Plan 模式、todo.md、Architect/Editor 分工
Memory 记忆 跨步骤、跨会话保住状态 第 20 步忘掉第 2 步的约束 messages、CLAUDE.md、向量索引、scratchpad
Tools 工具 把外部世界变成可调用函数 只能吐文本,不能碰仓库 read/write/grep/bash、MCP Server
Executor 执行器 真正跑命令、改磁盘 工具调用停在 JSON 里 本地 subprocess、Docker、微虚拟机、git worktree
Reflection 反思 根据失败改策略,而不是原地重试 同一处报错循环撞击 Reflexion 笔记、测试失败分析、自评 rubric

它们不是五层蛋糕,而是焊在循环不同相位上的插件:
flowchart LR subgraph loop "单次循环" P"Planning\
必要时先出计划"
--> T"Thought" T --> A"Action = Tools" A --> E"Executor" E --> O"Observation" O --> M"写入 Memory" M --> R"Reflection\
要不要改方向"
R --> T end

对编码 Agent,每一件都有非常具体的样子。

规划。

纯 ReAct 是近视的:每步只优化「此刻最合理的下一个动作」。修一个 20 文件的重构,近视循环会在中途改口、重复劳动、甚至把刚修好的东西改回去。所以生产工具几乎都加了 Plan-then-Execute:先产出一份「改哪些文件、什么顺序、完成标准」,人批准后再执行。Aider 更进一步,用强模型当 Architect 出方案,用另一个(可更小的)模型当 Editor 出补丁。计划还有一个隐藏价值:上下文压缩之后,早期对话细节会丢,但计划可以当作锚点留在窗口里,避免 Agent 失忆后改去做另一件事。

记忆。

至少分五层,初学者不要一上来就上向量数据库:

  1. 工作记忆 :当前 messages。寿命 = 这一次会话。最贵,也最准。
  2. 项目记忆CLAUDE.mdAGENTS.md.cursorrules。人写的「这个仓库怎么干活」:构建命令、目录约定、不许碰的目录。
  3. 过程记忆 :Agent 自己写的 todo.md / memory.md / scratchpad。长任务中途用来对抗失忆。
  4. 语义记忆:仓库向量索引、符号图谱。用来回答「认证逻辑在哪」这种自然语言问题。
  5. 经验记忆:跨任务的失败案例、团队规范摘要。2026 年部分系统开始做定期「复盘」(有的称为 Dreaming):从历史会话里提炼模式,写回编排记忆。

Anthropic 给过一个很朴素但极其有效的模式:让模型把笔记写到上下文窗口外面的文件里,需要时再读回来。 这比幻想无限上下文便宜得多,也稳定得多。

工具。

工具是 Agent 的手。手太少,它只能空想;手太多、描述含糊,它会在「该用 grep 还是该用 codebase_search」之间烧轮次。后文架构部分会专门讲 MCP 和工具膨胀。这里先记一条经验法则:人如果都说不清此刻该用哪一个工具,模型更说不清。第一版工具集,永远应该比你想的更小。

执行器。

JSON 工具调用只是意图。真正改磁盘、跑测试的是执行器。执行器决定 Agent 的「身体」:没有它,Agent 是嘴;有了 bash 和文件系统,Agent 才是工程师;有了浏览器,它才能自己查文档、点 UI;有了隔离的 git worktree 或云虚拟机,它才能并行,而不把你正在改的分支改炸。

反思。

最便宜的反思是把 stderr 原样丢回模型。更好的反思是强制它先写三行:什么失败了、根因假设、下一步不重复的策略。Reflexion(Shinn 等,2023)把这三行叫做「反思笔记」,贴进下一次 prompt。很多「Agent 很笨、一直重试同一条命令」的现场,缺的不是更强模型,而是这一段强制转向。

缺任何一件的后果可以记成口诀:

没有规划就是聊天机器人;没有记忆就忘事;没有工具就只会写字;没有执行器就空想;没有反思就一条路撞死。

2.4 上下文工程:决定 Agent 成败的真正战场

到 2025 年中,有经验的人已经承认:提示词不再是主瓶颈。主瓶颈是------每一轮推理时,到底往窗口里塞什么。 这件事现在有名字:上下文工程(Context Engineering)。

Anthropic 的定义很干净:在推理时,策展并维持那一组「刚好够用」的 token。提示词工程管的是一句话怎么说;上下文工程管的是这句话周围整条管道:系统指令、检索到的代码、工具定义、历史、记忆、输出格式。

对编码 Agent,这一点被放大到残酷。聊天机器人一轮就结束;编码 Agent 可能在第 47 步做决策,而第 1 到 46 步的残渣还堆在窗口里。token 预算有限,注意力预算更有限。Chroma 等机构的研究表明,上下文质量从窗口大约 25% 开始就会下降 ,而不是等到 100% 才崩。这叫 context rot:塞得越多,模型越记不住中间那截真正重要的东西。2023 年那篇 Lost in the Middle 已经证明,关键信息放在开头或结尾时表现最好,埋在长上下文中间会显著变差。所以「我有 100 万 token 窗口,把整个仓库灌进去」不仅贵,而且常常更蠢。

编码 Agent 的上下文,通常由四块拼出来:
flowchart TB subgraph budget "有限的上下文窗口" S"系统提示 + 项目约定\
CLAUDE.md / 安全规则"
T"工具定义\
越少越好、描述必须互斥"
H"对话与工具历史\
可压缩、可摘要"
R"本轮检索到的代码\
文件 / 仓库地图 / 符号定义"
end S --> LLM T --> LLM H --> LLM R --> LLM LLM"模型本轮推理"

四根柱子对应四个工程问题:指令、检索、记忆、工具。对编码任务,检索这一柱最容易出事故。一个典型失败是:在百万行单体里 grep 某个符号,返回 4000 条命中,Agent 把窗口烧在无关文件上,真正的根因从头到尾没进过上下文。模型不是不会修,是没看见该修的地方。

业界大概形成了三种互补的检索策略:

仓库地图(Aider 代表)。

用 tree-sitter 把仓库解析成「类名、函数签名、引用关系」的缩略图,函数体先不放进来。一份大仓库可以被压成几千 token 的结构说明书。真正要改某个文件时,再把该文件全文塞进去。两级视野:地图负责定向,全文负责动手。适合中小型仓库,实现简单,token 效率极高。

向量 / 语义索引(Cursor 代表)。

把代码块做成 embedding,用「认证逻辑在哪」这种自然语言去搜。对说人话的查询很友好,但对「这个符号的唯一定义点」不如编译器级索引稳。索引还会过期:上季度 embed 的块,可能已经不是生产里跑着的函数。

代码图谱 / SCIP 式精确导航(Sourcegraph 等)。

问的是 RecordAccumulator 的定义,返回的就是定义文件加三处调用点,而不是 50 个碰巧包含这个字符串的文件。在企业级多仓场景,这往往是从「超时」到「几分钟内完成」的差别。

无论哪一种,上下文装配都必须有裁剪纪律:

  • 工具输出截断(一个 2 万行的测试日志,只留失败附近)。
  • 旧对话压缩成「已做决策 + 关键路径 + 未完成项」。
  • 检索结果设相关度阈值和 top-k。
  • 高信号内容放窗口两端,不要堆在中间。

还有一个 2026 年被反复打脸的坑:工具定义本身就会吃窗口。 有报告称,三个 MCP Server 的工具描述就能吃掉 20 万窗口里的 14 万 token------用户问题还没进场。工具选择准确率会随着工具集膨胀显著下降。后文会把「工具要少、描述要互斥、按需加载」写成硬约束。

所以,把上下文工程记成四句口诀已经够用:

取什么,何时取,如何压,何时扔。

改提示词是在调措辞;改这四句,才是在调 Agent。

2.5 Function Calling、CodeAct 与代码怎么被改进仓库

循环决定 Agent「还干不干」,工具调用格式决定它「怎么动手」,编辑策略决定「改动能不能稳稳落到文件上」。这三件事经常被混成一句「模型会改代码」,必须拆开。

Function Calling:给行动一个 JSON 插座

现代模型并不在文本里自由发挥「我要调用 read_file」。它们在训练和对齐阶段就学会了输出结构化的 tool_calls:名字 + 参数。编排器校验参数,执行,把结果以 role=tool 的消息写回。这是 ReAct 在 API 层的实现。

一次典型往返:

json 复制代码
{
  "role": "assistant",
  "tool_calls": [
    {
      "id": "call_1",
      "type": "function",
      "function": {
        "name": "read_file",
        "arguments": "{\"path\": \"src/auth/login.py\"}"
      }
    }
  ]
}

执行器返回:

json 复制代码
{
  "role": "tool",
  "tool_call_id": "call_1",
  "content": "def login(user):\n    ..."
}

模型下一轮看到这份 Observation,再决定是继续读测试,还是直接 edit_file

Function Calling 的优点是约束强、编排器好写、便于权限拦截。缺点是每一步都要出一次 JSON、中间结果必须回到上下文,多步数据变换会反复烧 token。

CodeAct:让行动变成一段 Python

2024 年 Wang 等人提出 CodeAct:不要每次只调一个被 schema 绑死的工具,让模型直接写一段可执行代码,工具以函数形式存在于这段代码的运行时里。

JSON 工具调用:

json 复制代码
{"tool": "search_menu", "args": {"restaurant_id": 42}}

CodeAct:

python 复制代码
menu = search_menu(restaurant_id=42)
cheap = [d for d in menu if d.price < 100]
return sorted(cheap, key=lambda d: d.rating, reverse=True)[:3]

循环、过滤、错误处理走的是 Python 语义,而不是外层编排器的 if-else。对数据分析、批量改文件、需要把三次工具结果捏在一起的任务,token 可以少一个数量级。2026 年,Anthropic 的 Programmatic Tool Calling、以及部分沙箱产品的 Code Mode,走的就是这条路:在一个可复用的容器里跑模型写的代码,状态可以跨步保留。

代价也清楚:执行面更大,沙箱必须更硬;模型写出的代码本身可能有 bug;调试「模型写的胶水代码」比调试一次 JSON 调用更麻烦。编码 Agent 的日常路径仍然是 Function Calling 为主、CodeAct 为辅------读改跑测用工具调用更可控,需要批量处理中间数据时再切到代码即行动。

四种把补丁打进文件的方法

这是编码 Agent 最有「手感」的架构决策,直接影响贵不贵、稳不稳、烂不烂。

1. 整文件重写。

模型输出改完后的全文,直接覆盖。无歧义,但改一行也要付 500 行输出 token。只适合短文件,或当模型搞不定 diff 格式时的回退。

2. 搜索替换块(Aider 默认)。

模型产出成对的「旧文本 / 新文本」。省 token,但空白、缩进、过期内容会导致匹配失败。Aider 用模糊匹配兜底。

text 复制代码
<<<<<<< SEARCH
    return address.getStreet();
=======
    if (address == None):
        return ""
    return address.getStreet();
>>>>>>> REPLACE

3. 结构化 Edit 工具(Claude Code 一类)。

参数是 old_string + new_string,并带唯一性约束:旧串必须在文件中恰好出现一次。匹配 0 次或多次都返回错误,模型据此收紧上下文再试。错误发生在落盘前,比「写进去才发现改错了」安全。

4. AST 感知编辑(IDE 型 Agent,如 Cursor)。

按语法节点(函数、class、import)改,而不是按纯文本。对重命名、移动符号、保证括号匹配最稳,但要语言级解析器,实现成本最高。

可以记一张对照表:

策略 Token 成本 可靠性 适用
整文件重写 很高 高(无 diff 歧义) 小文件、模型不擅长 diff
搜索替换 中(怕空白和过期) 通用、跨模型
结构化 Edit 高(唯一性检查) 工具调用型 Agent
AST 编辑 最高 IDE 内、语言确定

到这里,原理层可以收口:

编码 Agent = 无状态 LLM + 有状态循环 + 被严格预算管理的上下文 + 一组少而清的工具 + 一种可靠的编辑格式 + 一个能跑测试的执行器。

模型提供判断;其余全部是软件工程。

下一节把这些零件装进生产级分层,并对照 2026 年的主流产品。


3. 架构剖析

简要介绍:原理告诉你循环为什么能转;架构告诉你转起来之后,怎样才不会把用户的仓库转穿。这一节给出一个足够用的分层模型,然后分别深入工具与 MCP、安全沙箱、多 Agent 编排,最后用同一套坐标对比 Claude Code、Cursor、Codex、Devin、Aider。你可以把这一节当成「自己做一套」或「选一套现成工具」时的检查清单。

3.1 生产级编码 Agent 的分层架构

一个能交付的编码 Agent,建议按六层来看。层与层之间有稳定接口:上面的层不应该直接操作磁盘,下面的层不应该理解「用户想修登录 NPE」这种业务目标。

text 复制代码
┌──────────────────────────────────────────────────────────┐
│  L6  体验与工作流层  终端 / IDE / 云看板 / 定时任务 / PR    │
├──────────────────────────────────────────────────────────┤
│  L5  编排与策略层    循环、计划、子 Agent、停止条件、Hooks │
├──────────────────────────────────────────────────────────┤
│  L4  协议层          MCP(Agent↔工具) A2A(Agent↔Agent)  │
├──────────────────────────────────────────────────────────┤
│  L3  上下文与记忆层  仓库地图、索引、压缩、项目约定文件     │
├──────────────────────────────────────────────────────────┤
│  L2  工具与执行层    read/write/grep/bash/浏览器 + 沙箱    │
├──────────────────────────────────────────────────────────┤
│  L1  模型层          推理、工具调用、(可选)extended thinking│
└──────────────────────────────────────────────────────────┘

L1 模型层。

2026 年的公开评测里,真实 GitHub issue 修复(SWE-bench Verified)已经从 2024 年的「能做几十个百分点」走到了「头部模型 80%--90% 区间」。例如 Claude Opus 4.8 在 2026 年 5 月公开口径下约 88.6%。必须同时记住三件事:分数强烈依赖 Harness(同样模型,迷你 Agent 和全量 Claude Code 差一截);SWE-bench Verified 已被认为接近饱和、存在污染争议,更难的 SWE-bench Pro 分数明显更低;你仓库里的单体、私有框架、脏 git 状态,比任何榜单都更接近真实。选模型看三轴:工具调用稳定性、长程不跑偏、成本延迟。编码 Agent 不是做阅读理解,是做 30 步之后还记得「不要改 migration 文件」的那种稳定性。

L2 工具与执行层。

最小充分集其实只有五个:list_dir / globread_fileedit_filegreprun_command。有了这五个,Agent 就能探索、修改、验证。再往上加 git、浏览器、LSP 诊断、包管理,自主性上升,爆炸半径也上升。执行层要处理超时、大输出截断、非零退出码、并行工具调用。Claude Code 一类实现会在同一模型回合里并行跑互不依赖的 grep 和 glob,探索阶段的墙钟时间会少一截。

L3 上下文与记忆层。

这一层是产品差距最大的地方。它回答:系统提示里写什么硬约束;项目级 CLAUDE.md 何时注入;仓库地图或索引何时更新;窗口快满时压缩算法保留什么(文件路径、已做决策、测试命令、未完成 todo,必须留;冗长的成功日志,必须扔)。压缩算法写不好,长任务会在第 20 步人格分裂。

L4 协议层。

2024 年底 Anthropic 开源 MCP(Model Context Protocol)之前,每个 Agent 都在自己的 main 函数里重写 GitHub、文件系统、数据库适配器。MCP 把「工具集成」从写代码变成写配置:Agent 当 client,工具当 server,中间 JSON-RPC。2026 年它已经覆盖数据库、SaaS、浏览器、内部系统。旁边还有 A2A(Agent 之间发现与交接)和 AG-UI(把「我正在干什么」流给前端)。对编码 Agent,MCP 是目前最值得接的那一层------但接的时候必须管住工具膨胀。

L5 编排与策略层。

就是 2.2 节那个 while 循环的工业化版本。要决策的包括:单循环还是计划后执行;要不要子 Agent;失败 N 次是否强制反思或升级模型;何时向人请求批准;Hooks 在「将要写文件 / 将要跑 bash」时能否拦截。Claude Code 把大量产品能力做成生命周期 Hooks 和 Skills,本质上都是在这一层插桩,而不是改模型。

L6 体验与工作流层。

同一套 L1--L5,装进终端就是 Claude Code / Codex CLI / Aider;装进 IDE 就是 Cursor / Windsurf(后并入 Devin Desktop 路线);装进云虚拟机就是 Devin / Copilot coding agent。2026 年的趋势是同一家产品同时占多层:Cursor 既有 Tab(毫秒级补全),也有 Composer(分钟级多文件),还有 Cloud Agents(小时级隔离虚拟机)。选工具越来越不是选能力,而是选你希望人站在哪一个自主性档位上。

把一次「修 bug」请求在六层里走一遍:
sequenceDiagram participant U as 用户 participant L6 as 体验层 participant L5 as 编排层 participant L1 as 模型 participant L3 as 上下文 participant L2 as 工具/沙箱 U->>L6: "修复登录 NPE 并保证测试通过" L6->>L5: 创建会话,注入目标与停止条件 L5->>L3: 装配系统提示、CLAUDE.md、仓库地图 L5->>L1: messages + tools L1-->>L5: tool_call: grep("getStreet") L5->>L2: 安全校验后执行 L2-->>L5: 命中 3 处文件 L5->>L3: 写入 Observation,必要时裁剪 L5->>L1: 下一轮 L1-->>L5: tool_call: edit_file + run_command("pytest") L2-->>L5: 测试失败日志 L5->>L1: 带失败日志再推理 L1-->>L5: 再编辑 + 再测 L2-->>L5: 全绿 L1-->>L5: 无工具调用,输出摘要 L5->>L6: diff / 提交说明 L6->>U: 请审核

自己做 Agent 时,不要从 L6 的花活开始。先让 L1+L2+L5 在一个目录里跑通「写文件 + 跑测试 + 根据失败修复」,再加 L3 压缩,再加 L4 MCP,最后才是 UI。倒过来做,你会得到一个看起来很 Agent、实际上只是套了皮肤的聊天框。

3.2 工具系统、MCP 与安全沙箱

这一小节解决两个会让系统当场死亡的问题:工具怎么接,以及工具跑起来后怎么防止把机器打穿。

工具设计的三条硬约束

约束 1:默认工具集要小。

最小充分集:globread_fileedit_filegreprun_command。这五个覆盖了 80% 的软件工程动作。搜索类工具(grep/glob)让上下文装配变成 Agent 驱动的,而不需要你预先指定文件。shell 让测试、构建、git、包管理不必各做一套 API。

约束 2:描述必须让人能做单选题。

如果 search_codegrep 的说明都是「搜索代码」,模型会掷骰子。正确做法是写清边界:grep 是正则精确匹配,适合符号和报错字符串;semantic_search 是自然语言问「逻辑在哪」,适合你不知道符号名的时候。Anthropic 有一句被反复验证的观察:人说不清该用哪个工具时,不要指望 Agent 能说清。

约束 3:结果必须对模型友好。

工具失败不要抛给编排器当未处理异常,而要返回可读的错误字符串:路径不存在、old_string 匹配到 3 处、命令超时、退出码 1 加 stderr 尾部 80 行。模型靠这些 Observation 转向。静默失败或返回巨大二进制,等于弄瞎它。

MCP:把工具从代码变成插座

MCP 的心智模型非常像 LSP(Language Server Protocol)对编辑器做的事:编辑器不必为每种语言重写智能,语言把能力做成 server。Agent 不必为每个 SaaS 重写适配器,SaaS 把能力做成 MCP Server。
flowchart LR Agent"编码 Agent\
MCP Client"
-->|"JSON-RPC<br/>tools/list, tools/call"| S1"filesystem" Agent --> S2"github" Agent --> S3"postgres" Agent --> S4"playwright" Agent --> S5"公司内部工具"

对编码场景,一组高价值 server 通常是:

  • 文件系统 / git / GitHub:读仓、开 PR、看 CI。
  • 当前文档(避免模型用过期 API)。
  • 数据库只读查询(看真实数据长什么样)。
  • Playwright:改完 UI 自己点一遍。

接入 MCP 时有一个反直觉的生产经验:不要把所有 server 的全部工具一开场就塞进系统提示。tools/list 拿到名字和一句话描述,真正要调用时再拉完整 schema。有的实现宣称这样能把工具占用的上下文砍掉大半。这叫按需披露(progressive disclosure),和「Skills 用 Markdown 写、用到再加载」是同一设计哲学。

安全不是售后附件,是架构决策

编码 Agent 拥有的权限,接近「一个能执行任意命令的实习生」。安全模型一旦事后补,一定补不住。主流几条路,各有代价:

模型 代表 做法 优点 代价
白名单 + 当场批准 Claude Code 不在 allowlist 里的命令弹确认;可「本次 / 本会话 / 永久」 灵活,熟仓库可放开 要设计好默认拒绝策略
每步人批 Cline 每个工具调用先过眼睛 最保守 复杂任务会点到手酸
Git 即撤销 Aider 每次编辑自动 commit 回滚是一条命令 历史会被小提交污染
IDE 沙箱 + diff 审 Cursor 终端进沙箱,改动以未保存 diff 呈现 符合 IDE 手感 关了窗口,后台任务怎么算要单独设计
云虚拟机隔离 Devin / Cloud Agents 在别人的 VM 里折腾,PR 打回来 爆炸半径不在你笔记本上 环境还原、密钥注入、异步等待

无论选哪条,执行器里至少要有这些机械检查(它们不依赖模型「听话」):

  1. 路径规范化后必须落在项目根内,拒绝 .. 穿越。
  2. 禁止或二次确认高危命令:rm -rfgit push --forcedrop table、改 .ssh、改仓库外路径。
  3. 命令超时(30--120 秒起步),stdout/stderr 截断。
  4. 密钥不进提示词:用沙箱侧的注入或受限环境变量,而不是让模型「记得把 token 写进命令」。
  5. 网络默认最小权限。需要查文档再白名单域名。

2026 年隔离技术的默认选项正在从「普通容器」走向 microVM (Firecracker 一类):启动几百毫秒,内存开销远小于传统虚拟机,隔离性又强于共享内核容器。Claude Code 本地还有一个更轻的并发隔离:git worktree------每个子 Agent 一份工作树、一个分支,合流前互不踩踏。你在单机上复现多 Agent,优先用 worktree,而不是上来就上 Kubernetes。

最后补一条常被忽略的安全边界:模型输出不是可信输入。 即便工具名是 read_file,参数也要当攻击者输入来看。提示词注入可以从 README、issue、网页抓取结果里来。生产级 Harness 会把「来自外部的 Observation」标成不可执行指令,只当数据。这不是偏执,这是 2024 年以来真实出过的事故模式。

3.3 多 Agent、Plan-Execute 与 2026 年主流产品对照

单循环在「改一个函数、补一个测试」时非常漂亮。到「把 20 个文件的认证中间件换掉,同时改文档和 CI」时,它会在两方面同时破产:窗口装不下全过程;错误会沿步骤链累积。于是出现两类演化。

Plan-then-Execute。

先完整规划,再逐步执行。计划本身是一份可批准、可压缩后仍保留的契约。Cline 的 Plan / Act 双模式、Claude Code 的 Plan 模式,都是这个思路。对人的价值是:你审核的是方案,不是 40 次工具调用现场。

子 Agent 委派。

主 Agent 把独立子任务(「迁移数据库层」和「改 HTTP handler」)交给拥有独立上下文窗口 的子 Agent,子 Agent 只回摘要,不把全部中间日志打回主窗口。Cursor 允许并行后台 Agent;Claude Code 的 Dynamic Workflows 把这件事推到「一次会话里拉起大量并行子 Agent」;实践者经验是:人还能认真审核的并行度大约是 3--5 个,而不是几百个。 吞吐上限不在模型,在人的注意力和合流冲突。

Addy Osmani 把这种工厂式流程写成六步,很适合当团队 SOP:

text 复制代码
Plan → Spawn agents → Monitor → Verify → Integrate → Retrospective

对应到 git,就是:主仓只接受经过验证的 PR;每个 Agent 在自己的 worktree / 分支上工作;合流靠测试和人工审核,不靠信任模型的「我做完了」。

用同一套坐标看主流产品

不要再问「哪个最强」。问「人站在回路的哪一截」。

维度 Claude Code Cursor Codex CLI Devin 类 Aider
主表面 终端(也可进 IDE) AI 原生 IDE 终端 / 云 云端虚拟机 + 看板 终端
循环 单线程主循环 + 子 Agent Tab / 行内编辑 / Composer / Cloud 多层 Agent 循环 + 云端并行环境 高自主、异步 Architect + Editor
上下文 项目 md + 即时检索 + 压缩 仓库语义索引 + 多模型 仓内工具 + 云端副本 完整 VM 工作区 tree-sitter 仓库地图
编辑 结构化 Edit,唯一性校验 多文件 + AST / diff 审阅 补丁 / 文件级 自主改,PR 回来 搜索替换,git 每步提交
安全 白名单 + 批准 + Hooks IDE 沙箱 + diff 沙箱执行 云隔离 git 回滚
最擅长 深重构、长程调试、可编程 Harness 日常结对、可视化 diff、快速切换模型 异步清 backlog、PR 流 规格清楚的后台任务 便宜、可复现、模型无关
主要代价 token 猛、终端心智 订阅贵、上下文漂移 沙箱边界、会话衔接 规格糊就空转、费用按量 终端能力不如前两者完整

2026 年中的一个经验配置是组合拳,而不是信仰单一品牌:

  • 日常击键和局部重构:Cursor Tab + Composer。
  • 跨模块、要跑测试、要看 git 历史的深活:Claude Code。
  • 规格清楚的 issue 清扫:云端 Codex / Copilot coding agent / Devin。
  • 想把每次改动都留在 git 里、或离线用开源模型:Aider。

它们能共存,是因为都作用在「普通文件 + git」上。冲突来自同时写同一分支。并发时请分支隔离,这不是 AI 问题,这是 1970 年代以来的配置管理问题。

再补一条评测读写方法,避免被营销带跑。SWE-bench Verified 测的是「给定 issue 和仓库快照,生成能过测试的 patch」。它有用,因为它用真实测试而不是人工偏好。它不够用,因为:任务偏 Python 开源热门仓;Harness 差异巨大;高分模型可能见过类似数据;它几乎不测你公司那种 20 个仓互相依赖、构建要 15 分钟、规范写在已经离职的人口头上的现场。把榜单当筛选漏斗,把你仓库里的 20 个真实 issue 当验收场。

架构部分的收口:

生产级编码 Agent 的差异,90% 不在模型商标,而在:上下文怎么选、工具怎么少而清、编辑怎么可逆、失败怎么转向、人站在哪一层审核。

你如果只能优化一件事,优化上下文和停止条件,而不是再换一个「更强」的模型。


4. 实践

简要介绍:前面把原理和架构讲完。这一节做三件实事:说明编码 Agent 现在真正能扛的工作类型;给出一份从零可运行的实现(先最小循环,再补上生产级零件);最后谈失败模式、以及开发者在接下来几年应该把自己放在哪一个位置。代码刻意不绑死某一家云,使用 OpenAI 兼容接口,你可以把 base_url 指到自家网关、开源模型或任何提供 Function Calling 的服务。

4.1 编码 Agent 现在真正能干什么

先划能力圈,避免把 Agent 当成万能实习生。

圈内:闭环短、验证硬、规格清的任务。

  • 修复带复现路径的 bug:有测试或有明确报错。
  • 补测试、补类型、补文档、做机械式重构(改名、拆文件、升级依赖)。
  • 按现有目录约定脚手架:新 CRUD 接口、新页面、新 GitHub Action。
  • 解释一段陌生代码:先 grep 再读,比人肉翻快。
  • CI 失败后的「读日志 → 改 → 再跑」循环。
  • 规格清楚的迁移:例如「把错误处理统一换成我们的 AppError」,有 lint 或测试当裁判。

圈边:能做,但必须人盯着。

  • 跨 10+ 文件的架构改动。Agent 可以出计划和第一轮补丁,合流和取舍必须人做。
  • 性能优化。它能改,但「快了没有」要靠你给基准,而不是靠它的自我感觉。
  • 涉及密钥、支付、权限的改动。可以让它写,不允许它自己合。

圈外:现在仍不该全权委托。

  • 目标含糊:「把代码写得更好看一点」。
  • 没有验证器:改完无法跑测试、无法预览、无法 diff 审。
  • 需要原创产品判断:「我们该不该做这个功能」。
  • 安全敏感的生产操作:对真实用户数据跑迁移、强推 git、改基础设施销毁策略。

把用途映射到工作流,推荐三条,从保守到激进:
flowchart TB subgraph A "结对模式:人在键盘上" A1"人写意图" --> A2"Agent 改当前分支" A2 --> A3"人看 diff,跑一遍自己关心的路径" end subgraph B "工单模式:人在审核位" B1"Issue 写清验收标准" --> B2"Agent 在独立分支 / worktree 干完" B2 --> B3"CI + 人审 PR" end subgraph C "值班模式:人在规则位" C1"定时或 CI 失败触发" --> C2"Agent 尝试修复" C2 --> C3{"验证器通过?"} C3 -->|是| C4"自动开 PR" C3 -->|否| C5"失败上报,不落主分支" end

给 Agent 写任务时,把「给实习生的工单」标准套上去就对了。一份合格的任务至少有四段:

  1. 目标 :一句话,可判定真假。
    「给 parse_config 补上缺失键时抛 ConfigError,而不是返回 None。」
  2. 范围 :哪些目录可以动,哪些绝对不能动。
    「只改 src/config/tests/config/,不要动 src/legacy/。」
  3. 验证 :Agent 自己能跑的命令。
    pytest tests/config -q 必须全绿。」
  4. 完成态 :怎样算停。
    「测试绿、不新增依赖、最后用 5 行中文总结改了什么。」

你不写这四段,Agent 就会优化一个你没说出口的目标:看起来忙、输出很长、改动很多。那不是智能,那是无目标系统的默认行为。

4.2 从零手写一个可运行的编码 Agent

下面这份实现刻意保持在「一个文件能讲完」的体量,但已经包含生产循环的全部关键零件:

  • OpenAI 兼容的 原生 Function Calling(不要让模型在文本里手写 JSON,解析会碎)。
  • 五个工具:列目录、读文件、写入、搜索、跑命令。
  • 项目根沙箱:路径穿越直接拒绝。
  • 命令超时与输出截断。
  • 最大步数。
  • 把每一轮工具调用打印出来,方便你观察循环。

先安装依赖:

bash 复制代码
pip install openai
export OPENAI_API_KEY=sk-your-key
# 可选:指向兼容网关
# export OPENAI_BASE_URL=https://your-gateway/v1
# export OPENAI_MODEL=gpt-4o

把下面存为 mini_coding_agent.py。它不是玩具演示用的伪代码,而是可以在一个真实目录里改文件、跑 pytest 的最小 Harness。

python 复制代码
#!/usr/bin/env python3
"""最小可运行编码 Agent:ReAct 循环 + 沙箱工具 + Function Calling。"""

from __future__ import annotations

import json
import os
import subprocess
from pathlib import Path
from typing import Any

from openai import OpenAI

ROOT = Path(os.environ.get("AGENT_ROOT", ".")).resolve()
MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o")
MAX_STEPS = int(os.environ.get("AGENT_MAX_STEPS", "20"))
CMD_TIMEOUT = int(os.environ.get("AGENT_CMD_TIMEOUT", "30"))
MAX_OUTPUT = 8000  # 防止单次工具结果撑爆窗口

SYSTEM_PROMPT = f"""你是一个谨慎的编码 Agent,工作目录是:{ROOT}

规则:
1. 先探索再修改。不确定文件在哪时,使用 glob 或 grep。
2. 每次修改后,只要存在测试或可以运行的命令,就必须验证。
3. 优先做最小改动,不要引入新依赖,不要重构无关代码。
4. 任务真正完成后,直接用自然语言总结,不再调用工具。
5. 如果你连续两次用同一命令得到同一错误,必须改策略或向用户说明阻塞点。
"""

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "glob",
            "description": "按 glob 模式列出工作目录内的文件。适合找文件,不适合搜文件内容。",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {"type": "string", "description": "例如 **/*.py"}
                },
                "required": ["pattern"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "read_file",
            "description": "读取工作目录内一个文本文件的内容。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "offset": {"type": "integer", "description": "从第几行开始,从 1 计,可选"},
                    "limit": {"type": "integer", "description": "最多读多少行,可选"},
                },
                "required": ["path"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "write_file",
            "description": "写入(覆盖)工作目录内一个文本文件。目录不存在时会创建。",
            "parameters": {
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"},
                },
                "required": ["path", "content"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "grep",
            "description": "在工作目录内用正则搜索文件内容,返回 path:line:匹配文本。适合定位符号和报错。",
            "parameters": {
                "type": "object",
                "properties": {
                    "pattern": {"type": "string"},
                    "glob": {"type": "string", "description": "可选,例如 *.py"},
                },
                "required": ["pattern"],
            },
        },
    },
    {
        "type": "function",
        "function": {
            "name": "run_command",
            "description": "在工作目录内执行一条 shell 命令,返回退出码、stdout、stderr。用于测试、构建、运行脚本。禁止删除仓库外文件或强推 git。",
            "parameters": {
                "type": "object",
                "properties": {"cmd": {"type": "string"}},
                "required": ["cmd"],
            },
        },
    },
]


def safe_path(raw: str) -> Path:
    """拒绝路径穿越:解析后必须仍在 ROOT 内。"""
    path = (ROOT / raw).resolve() if not Path(raw).is_absolute() else Path(raw).resolve()
    try:
        path.relative_to(ROOT)
    except ValueError as exc:
        raise PermissionError(f"path escapes workspace: {raw}") from exc
    return path


def clip(text: str, n: int = MAX_OUTPUT) -> str:
    if len(text) <= n:
        return text
    return text[:n] + f"\n...[truncated {len(text) - n} chars]"


def tool_glob(pattern: str) -> str:
    matches = sorted(str(p.relative_to(ROOT)) for p in ROOT.glob(pattern) if p.is_file())
    return "\n".join(matches[:400]) or "(no matches)"


def tool_read_file(path: str, offset: int | None = None, limit: int | None = None) -> str:
    text = safe_path(path).read_text(encoding="utf-8")
    lines = text.splitlines()
    start = (offset - 1) if offset else 0
    end = (start + limit) if limit else len(lines)
    sliced = lines[max(0, start):end]
    numbered = [f"{i + 1 + max(0, start)}|{line}" for i, line in enumerate(sliced)]
    return clip("\n".join(numbered))


def tool_write_file(path: str, content: str) -> str:
    p = safe_path(path)
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content, encoding="utf-8")
    return f"wrote {len(content)} bytes -> {p.relative_to(ROOT)}"


def tool_grep(pattern: str, glob: str | None = None) -> str:
    cmd = ["rg", "-n", "--hidden", "--glob", "!.git", pattern]
    if glob:
        cmd.extend(["--glob", glob])
    try:
        proc = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True, timeout=20)
        out = proc.stdout or proc.stderr or "(no matches)"
        return clip(out)
    except FileNotFoundError:
        # 没有 ripgrep 时退回 Python,慢但能跑
        import re

        rx = re.compile(pattern)
        hits: list[str] = []
        for file in ROOT.rglob(glob or "*"):
            if not file.is_file() or ".git" in file.parts:
                continue
            try:
                for i, line in enumerate(file.read_text(encoding="utf-8", errors="ignore").splitlines(), 1):
                    if rx.search(line):
                        hits.append(f"{file.relative_to(ROOT)}:{i}:{line}")
                        if len(hits) >= 200:
                            return clip("\n".join(hits))
            except Exception:
                continue
        return clip("\n".join(hits)) or "(no matches)"


DANGEROUS = ("rm -rf /", "git push --force", "git reset --hard", "mkfs", "sudo ", ":(){")


def tool_run_command(cmd: str) -> str:
    lowered = cmd.strip().lower()
    if any(bad in lowered for bad in DANGEROUS):
        return f"blocked dangerous command: {cmd}"
    proc = subprocess.run(
        cmd,
        shell=True,
        cwd=ROOT,
        capture_output=True,
        text=True,
        timeout=CMD_TIMEOUT,
    )
    return clip(
        f"exit={proc.returncode}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
    )


DISPATCH = {
    "glob": lambda **kw: tool_glob(kw["pattern"]),
    "read_file": lambda **kw: tool_read_file(**kw),
    "write_file": lambda **kw: tool_write_file(kw["path"], kw["content"]),
    "grep": lambda **kw: tool_grep(kw["pattern"], kw.get("glob")),
    "run_command": lambda **kw: tool_run_command(kw["cmd"]),
}


def run_agent(task: str) -> str:
    client = OpenAI()
    messages: list[dict[str, Any]] = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": task},
    ]

    for step in range(1, MAX_STEPS + 1):
        resp = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
            temperature=0.2,
        )
        msg = resp.choices[0].message
        messages.append(msg)

        if not msg.tool_calls:
            print(f"\n[done in {step} llm calls]\n")
            return msg.content or "(empty)"

        for call in msg.tool_calls:
            name = call.function.name
            args = json.loads(call.function.arguments or "{}")
            print(f"[{step}] {name} {args if name != 'write_file' else {'path': args.get('path')}}")
            try:
                result = DISPATCH[name](**args)
            except Exception as exc:  # 工具错误也要变成 Observation,供模型转向
                result = f"TOOL_ERROR: {type(exc).__name__}: {exc}"
            messages.append(
                {"role": "tool", "tool_call_id": call.id, "content": result}
            )

    return f"stopped at MAX_STEPS={MAX_STEPS}, last tools already executed"


if __name__ == "__main__":
    import sys

    user_task = " ".join(sys.argv[1:]) or (
        "在当前目录创建一个 calculator.py,实现 add/sub/mul/div。"
        "div 在除零时抛 ZeroDivisionError。"
        "再写 test_calculator.py,用 pytest 覆盖这四个函数和除零。"
        "运行 pytest 直到全绿,最后用中文总结。"
    )
    print(run_agent(user_task))

运行:

bash 复制代码
mkdir -p /tmp/agent-demo && cd /tmp/agent-demo
cp /path/to/mini_coding_agent.py .
python mini_coding_agent.py

你在终端里会看到类似:

text 复制代码
[1] glob {'pattern': '*.py'}
[2] write_file {'path': 'calculator.py'}
[3] write_file {'path': 'test_calculator.py'}
[4] run_command {'cmd': 'pytest -q'}
[done in 5 llm calls]

已创建 calculator.py 与 test_calculator.py,pytest 全绿......

这就是 2.2 节那张图的肉体:模型没有直接「生成一个项目给你看」,它是在循环里自己摸索、写入、用 pytest 当验证器,验证器说通过才停。把 AGENT_ROOT 指到你的真实仓库,再换一句带验收标准的任务,它就已经能做有限但真实的活。

一份项目约定文件,立刻提升「像团队成员」的程度

在仓库根放 AGENTS.md(或 CLAUDE.md),启动时读进来,拼到 SYSTEM_PROMPT 后面。这是成本最低、收益最高的上下文工程:

markdown 复制代码
# AGENTS.md

## 构建与测试
- Python 3.12,包管理用 uv
- 单测:`pytest -q`
- 不要提交 `.venv/` 和 `__pycache__/`

## 架构约定
- 业务逻辑放 `src/`,HTTP 层放 `src/api/`,不要在 handler 里写 SQL
- 错误统一抛 `AppError`

## 禁止
- 不要改 `src/legacy/`
- 不要新增生产依赖,除非任务明确要求
- 不要运行 `git push`

生产工具里这件事已经标准化。你自己的 200 行 Agent 只要在 run_agent 开头加:

python 复制代码
guide = ROOT / "AGENTS.md"
if guide.exists():
    messages[0]["content"] += "\n\n项目约定:\n" + guide.read_text(encoding="utf-8")[:6000]

模型就会少问很多「测试命令是什么」,也会少碰 legacy

4.3 从最小循环走到能上内部试用的 Harness

80 到 200 行的 Agent 能跑通演示,距离给同事用还有四道必须补的工序。下面给出可直接粘贴的增强,而不是口号。

(1)结构化编辑,避免整文件覆盖

write_file 留着当创建新文件用,日常修改改成 edit_file。唯一性校验能把大量「改错地方」挡在落盘前。

python 复制代码
def tool_edit_file(path: str, old_string: str, new_string: str) -> str:
    p = safe_path(path)
    text = p.read_text(encoding="utf-8")
    count = text.count(old_string)
    if count != 1:
        return (
            f"EDIT_REJECTED: old_string matched {count} times in {path}. "
            "It must match exactly once. Read the file again and narrow the snippet."
        )
    p.write_text(text.replace(old_string, new_string, 1), encoding="utf-8")
    return f"edited {p.relative_to(ROOT)}"

对应的 tool schema 把三个参数写清楚,并在 description 里写上「old_string 必须唯一」。模型第一次匹配失败后,通常会自己 read_file 再收紧片段。这就是 Claude Code 那类 Edit 工具的迷你版。

(2)强制反思,打断无限重试

记录最近几次命令签名。相同失败出现两次,就向 messages 注入一条系统级提醒:

python 复制代码
from collections import deque

recent_failures: deque[str] = deque(maxlen=4)

def remember_cmd_result(cmd: str, result: str) -> str:
    if "exit=0" in result:
        return result
    sig = f"{cmd}::{result[:200]}"
    recent_failures.append(sig)
    if list(recent_failures).count(sig) >= 2:
        return result + (
            "\nREFLECTION_REQUIRED: 同样的命令已经失败两次。"
            "请先用三句话写出:失败现象、根因假设、下一步不重复的策略。"
            "禁止再次执行完全相同的命令。"
        )
    return result

tool_run_command 返回前包一层即可。这是 Reflexion 论文在工程上最便宜的落地。

(3)上下文压缩,让长任务活过第 20 步

最朴素有效的压缩:当 messages 超过 N 条,把最早的工具结果替换成摘要,保留系统提示、用户任务、最近 K 轮原文。

python 复制代码
def compact(messages: list[dict], keep_last: int = 8) -> list[dict]:
    if len(messages) < 24:
        return messages
    head = messages[:2]  # system + user task
    tail = messages[-keep_last:]
    middle = messages[2:-keep_last]
    files, cmds = set(), []
    for m in middle:
        content = m.get("content") or ""
        if m.get("role") == "tool":
            if "wrote " in content or "edited " in content:
                files.add(content.split()[-1])
            if content.startswith("exit="):
                cmds.append(content.splitlines()[0])
    summary = {
        "role": "system",
        "content": (
            "以下是被压缩的早期过程摘要,细节已丢弃:\n"
            f"- 改过的文件: {', '.join(sorted(files)) or '无'}\n"
            f"- 早期命令结果: {'; '.join(cmds[-6:]) or '无'}\n"
            "请以用户原始任务和最近的工具结果为准,不要重复已经成功的编辑。"
        ),
    }
    return head + [summary] + tail

每一轮 LLM 调用前 messages[:] = compact(messages)。这不是学术级压缩,但足以让「修一个带测试的 bug」这种 15 步任务不在中途失忆。更进一步可以让模型自己写 scratchpad.md,压缩时保留「去读这个文件」而不是把笔记散落在历史里。

(4)测试闭环作为停止条件,而不是相信模型说「做完了」

编排器可以在模型宣称完成之后,再跑一次用户指定的验证命令。失败就不要把控制权交还用户,而是把失败当作新的 Observation 继续循环。

python 复制代码
VERIFY_CMD = os.environ.get("AGENT_VERIFY", "pytest -q")

def verify_or_continue(messages: list[dict]) -> str | None:
    result = tool_run_command(VERIFY_CMD)
    if "\nexit=0\n" in f"\n{result}" or result.startswith("exit=0"):
        return None  # 真的做完了
    messages.append({
        "role": "user",
        "content": (
            f"你声称任务完成,但验证命令 `{VERIFY_CMD}` 失败了:\n{result}\n"
            "请继续修复,直到验证通过。不要向用户汇报完成。"
        ),
    })
    return result

「模型说完了」是 chatbot 的停止条件;「验证器说完了」才是 Agent 的停止条件。这一点单独拎出来,是因为 80% 的演示 Agent 都停在了前一个。

(5)git worktree:给并行和回滚留后路

即使你还没有多 Agent,也建议每次任务在独立 worktree 里跑:

bash 复制代码
git worktree add ../repo-agent-login -b agent/fix-login-npe
AGENT_ROOT=../repo-agent-login python mini_coding_agent.py "..."
# 满意再合并;不满意 git worktree remove

这是 Aider「每次提交都可回滚」和 Claude Code「子 Agent 隔离」在单机上的最小公约数。

补齐这五件后,你手里的东西已经具备内部试用资格:循环、工具、沙箱、编辑约束、失败转向、压缩、外部验证器、git 隔离。再往上才是 MCP、IDE 插件、云虚拟机------那是产品化,不是原理缺口。

常见失败模式对照表

失败 你在终端里看到什么 真正原因 先做的修复
无限循环 同一 pytest 命令失败 8 次 错误信息含糊,又没有强制反思 失败两次注入 REFLECTION_REQUIRED;设最大步数
过度设计 你要一个解析器,它做了插件系统 任务没写「最小改动」 系统提示 + 任务里都写死范围
幻觉 API import 了一个不存在的包 知识截止或记混了相邻框架 写代码前 python -c "import X";接入当前文档 MCP
上下文崩塌 第 20 步开始撤销第 5 步的正确改动 窗口被日志淹没,早期约束丢失 compact;scratchpad;计划锚点
改错文件 同名函数改到了 legacy 检索太宽或没读约定 AGENTS.md 禁止目录;grep 后先 read 再 edit
工具选择抖动 在 grep 和 search 之间来回 工具描述重叠 删掉一个,或把边界写成单选题
看起来忙却没完成 很长的总结,测试没跑 停止条件是模型主观判断 外部 VERIFY_CMD 拦截「假完成」

这些不是模型性格问题,是 Harness 缺口。换更贵的模型可以掩盖一部分,但掩盖得很贵,而且一换任务就复发。


5. 总结

编码 Agent 看起来像魔法,拆开是一台很老派的机器。

机器的心脏是 ReAct 循环:模型看当前世界,决定一个动作,执行器真的去改世界,观察写回上下文,再看。机器的手是少而清的工具,通过 Function Calling 或 CodeAct 接到文件系统、shell、测试和外部系统。机器的眼睛是上下文工程:仓库地图、索引、压缩、项目约定,决定模型每一轮到底看见什么。机器的良心是权限、沙箱和 git 隔离。机器的裁判不是模型自己说「我做完了」,而是 pytest、lint、CI、diff 审核这些外部验证器。

所以:

  • 不要再用 Copilot 的心智去理解 Claude Code。Tab 键解决击键;循环解决任务。
  • 不要把「换更强模型」当成第一种优化。先写清目标、范围、验证、停止条件。
  • 不要一上来接 30 个 MCP、上多 Agent 编队。五个工具 + 沙箱 + 最大步数 + 测试闭环,已经能做真实工作。
  • 不要相信演示。用你自己仓库里的真实 issue 当评测,用 git 当悔棋盘。
  • 不要把人从回路里删掉。把人挪到回路中更贵的位置:定目标、定边界、定验证、承担风险。

本文给出的 mini_coding_agent.py 不是玩具,它是把上述句子翻译成可执行状态的最小集合。你可以从它开始,加上 edit_file、反思注入、上下文压缩、AGENTS.md、git worktree,在自己的仓库里跑通第一个「修 bug 直到测试全绿」的闭环。等你亲眼看过模型在第 4 步读到失败日志、在第 5 步改策略,而不是在聊天框里自信地撒谎,编码 Agent 就不再是一个营销词,而是你工具箱里一台可以理解、可以限制、可以改进的机器。

6. 结束语

这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

另外,博主出新书了《Hadoop与Spark大数据全景解析》、同时已出版的《深入理解Hive》、《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》也可以和新书配套使用,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。

相关推荐
图王大胜24 分钟前
万物演化论00(序章) 从宇宙到AI
人工智能·ai·宇宙·演化·文明·生命科学
技术小事1 小时前
How To Use Claude 怎么使用Claude
ai·claude
笑霸final1 小时前
不用上传服务器!用 Vue3 + ONNX Runtime Web 在浏览器本地实现智能抠图
运维·前端·图像处理·ai·onnx·canva可画·web性能优化
一切皆是因缘际会1 小时前
多时空虚拟中心—— 全域同源基座与脑机谐振应用架构
ai·系统架构·脑机接口·虚拟全球网络控制中心·元宇宙世界底层架构·原生多时空架构
新知图书1 小时前
16.3 基于MCP的多Agent旅行规划助手项目结构
人工智能·agent·ai agent·智能体
效率工作实验室2 小时前
AI Agent开发平台有哪些?2026年平台对比
ai·agent开发平台·ai智能体开发平台·智能体平台对比·agent平台选型
似水流年QC3 小时前
什么是 Skill?深入理解 AI Agent Skill 的工作原理与应用实践
人工智能·agent·skill
水管在开花.3 小时前
Agent范式与LangGraph④-零基础保姆级教程
人工智能·agent·rag