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 的开发者:你不需要先啃完论文,也能顺着比喻和流程图把原理看懂。另一类是准备自己做、或准备把现有工具用到极限的工程师:你会看到生产级架构怎么分层、主流产品怎么取舍、以及一份可以直接跑的实现。
读完你应该能回答五个问题:
- 编码 Agent 和 Copilot、ChatGPT 写代码,差在哪一层?
- 那个「会自己干活」的循环,到底在转什么?
- 为什么同样一个模型,套上不同 Harness(驾驭层)表现天差地别?
- 自己从零写一个能改文件、跑命令、根据测试结果自我修复的 Agent,最少要哪些代码?
- 接下来两年,开发者的工作会变成编排、审核和定标准,而不是和 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 失忆后改去做另一件事。
记忆。
至少分五层,初学者不要一上来就上向量数据库:
- 工作记忆 :当前
messages。寿命 = 这一次会话。最贵,也最准。 - 项目记忆 :
CLAUDE.md、AGENTS.md、.cursorrules。人写的「这个仓库怎么干活」:构建命令、目录约定、不许碰的目录。 - 过程记忆 :Agent 自己写的
todo.md/memory.md/ scratchpad。长任务中途用来对抗失忆。 - 语义记忆:仓库向量索引、符号图谱。用来回答「认证逻辑在哪」这种自然语言问题。
- 经验记忆:跨任务的失败案例、团队规范摘要。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 / glob、read_file、edit_file、grep、run_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:默认工具集要小。
最小充分集:glob、read_file、edit_file、grep、run_command。这五个覆盖了 80% 的软件工程动作。搜索类工具(grep/glob)让上下文装配变成 Agent 驱动的,而不需要你预先指定文件。shell 让测试、构建、git、包管理不必各做一套 API。
约束 2:描述必须让人能做单选题。
如果 search_code 和 grep 的说明都是「搜索代码」,模型会掷骰子。正确做法是写清边界: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 打回来 | 爆炸半径不在你笔记本上 | 环境还原、密钥注入、异步等待 |
无论选哪条,执行器里至少要有这些机械检查(它们不依赖模型「听话」):
- 路径规范化后必须落在项目根内,拒绝
..穿越。 - 禁止或二次确认高危命令:
rm -rf、git push --force、drop table、改.ssh、改仓库外路径。 - 命令超时(30--120 秒起步),stdout/stderr 截断。
- 密钥不进提示词:用沙箱侧的注入或受限环境变量,而不是让模型「记得把 token 写进命令」。
- 网络默认最小权限。需要查文档再白名单域名。
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 写任务时,把「给实习生的工单」标准套上去就对了。一份合格的任务至少有四段:
- 目标 :一句话,可判定真假。
「给parse_config补上缺失键时抛ConfigError,而不是返回 None。」 - 范围 :哪些目录可以动,哪些绝对不能动。
「只改src/config/和tests/config/,不要动src/legacy/。」 - 验证 :Agent 自己能跑的命令。
「pytest tests/config -q必须全绿。」 - 完成态 :怎样算停。
「测试绿、不新增依赖、最后用 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大数据挖掘从入门到进阶实战》也可以和新书配套使用,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。