1. 先把问题想明白:自我编程Agent到底解决什么
去年年底,我手上有二十多个零散的小工具,为了省事想做一套基于大语言模型的自我编程Agent系统,让模型自己读代码、改代码、跑测试。这个想法最终落地成一个能自动完成编程任务的工作流系统,整个过程中踩了不少坑,也沉淀出一些方法论。今天我把整个构建过程完整复盘一遍,从架构设计、工具链选型到核心模块的实现方式,尽量写得能让别人照着复现。
先说清楚这套系统适合谁。如果你只是想用大模型帮你补全几行代码、写个简单脚本,那你不需要Agent,一个带代码补全的编辑器就够了。但如果你希望模型能独立完成“需求理解—任务拆解—代码编写—执行验证—自我修复”这样一整条闭环,甚至在无人值守的情况下连续工作几十分钟,那你就需要一套更完整的工程框架。这正是自我编程Agent的定位:它不是“帮你写代码的助手”,而是一个“能自己完成开发任务并验证结果的可执行系统”。
我从这个实践中最大的体会是:难点从来不是“让模型写代码”这一步,而是如何让它写的代码能被验证、能落地、能在一个可控的边界里持续迭代。模型的输出天生带有不确定性,整套系统的价值恰恰在于用工程手段把这种不确定性约束在安全范围内。
1.1 它和“AI写代码”有什么本质区别
很多人把自我编程Agent等同于“用大模型写代码”,这个理解偏差会带来大量无效尝试。
常规的AI编程辅助工具,工作模式是你提出需求,模型返回一段代码片段,然后由人负责把这段代码放进项目、运行、调试、修复。这个过程中模型只承担了“生成”这一环节,剩下的验证和纠错全部靠人肉完成。自我编程Agent则完全不同,它至少包含三个独立能力:任务规划能力、工具调用能力和自我验证能力。
任务规划能力让模型能把一个模糊需求拆成多个可执行的子任务。比如“做一个批量格式转换工具”这句话,人可以在脑子里自动拆解成“读取输入文件、解析内容结构、输出目标格式、处理异常分支”,但模型需要一个显式的机制去完成这种拆解,而且拆解结果最好能落成结构化的计划,而不是停留在对话里。
工具调用能力解决了“模型怎么影响真实世界”的问题。代码不只是文本,它要被写进文件系统、被执行、被测试。Agent必须能够读取文件、写入代码、执行命令、运行测试,甚至操作版本控制工具。这些能力在大模型API里不会自动出现,你得显式定义一个工具层。
自我验证能力是整套系统的灵魂。模型写完代码之后,必须能自动运行测试、解析结果、识别失败原因,并把错误信息反馈到下一轮处理中。没有这个闭环,模型就会不断“生成”看似合理但根本跑不起来的代码。
所以一个真正可用的Agent系统,本质上是“大模型+工具+执行环境+反馈回路”的组合体,任何一块缺失都会导致整个链条断掉。
1.2 方案选择:为什么不能让模型一次性生成最终代码
在我早期实验阶段,试过最简单的方案:把整个项目需求写进系统提示词,要求模型一次性输出完整代码,然后人工执行并修复。实验结果非常稳定地失败。
原因在于代码生成的复杂性超过了单次生成的上限。一个中等规模的功能模块,涉及文件I/O、异常处理、依赖管理、命令解析等至少十几个决策点。任何一个决策点出错,最终代码都会出现结构性错误。而模型对自身错误的感知能力很差,它会在一个错误假设上继续堆代码,最后生成一份“看起来很完整,实际完全不可运行”的产物。
更关键的是,编程的过程天然是迭代式的。人类开发者在写代码时会不断运行、调试、发现问题、修改,这个循环平均要进行五到十轮。如果你把人类开发者的任务也设计成“一次性写完全部代码”,质量同样会崩。自我编程Agent必须复刻这个迭代过程,把写代码变成“小步提交、快速验证、失败反馈、持续修正”的循环。
这里的实现方式主要有两条路线。一条是效果最直接的工具调用循环,由Agent反复决策“下一步执行什么工具”,把每一步的结果填入上下文,直到任务完成为止。另一条是预先定义多个角色,比如规划者、执行者、审查者,用多轮交互模拟一个微型开发团队。两种路线我都测试过,结论比较明确:单Agent加工具循环更适合任务边界清晰的场景,多角色协作则在复杂任务上更稳定,但代价是更高的Token消耗和更长的运行时间。
1.3 边界划分:哪些事必须留给人来拍板
设计这套系统的过程中,我给自己定了一条铁律:Agent负责执行,人负责定义边界。这不是保守,而是对失效模式的清醒认知。
首先,需求定义权必须留给人。模型理解需求的能力再强,也不能替用户决定产品的优先级和验收标准。需求描述模糊时,Agent最可能做的事就是猜一个最省事的方案。比如“让程序变快”,它可能会直接往代码里塞一个缓存,而不是去分析瓶颈在哪里。所以我的个人实践是,Agent只接受明确的目标描述,不确定的部分必须通过配置或约束输入提前固定下来。
其次,风险决策必须留给人。涉及删除数据、覆盖文件、安装依赖、修改环境配置这类高风险操作,Agent应当停下来等待人工确认。我见过一次很真实的事故:某测试脚本让Agent“清理临时文件”,它直接把整个项目的dist目录删掉了,连备份都没有。Agent不理解“临时文件”和“构建产物”之间的细微差异,它只是执行了命令。
再次,最终验收必须留给人。测试用例全绿只能说明系统的行为符合测试断言,不代表产品真的满足用户需求。我会把Agent的产出定位为“可评审的代码变更”,而不是“可直接交付的最终系统”。代码审查、部署决策这些环节,永远需要人在流程中把关。
1.4 模块化架构:Planner、Worker、Critic三个角色
在设计了十几个无法落地的方案之后,最终采用的架构是三个角色的协同:Planner负责规划,Worker负责执行,Critic负责检验。
Planner接收用户的目标描述,输出一份结构化的任务计划。它不写代码,只做分解。比如目标是“开发一个支持递归扫描的批量文件重命名工具”,Planner会输出类似这样的步骤:第一,定义命令行参数接口;第二,实现目录递归遍历;第三,实现重命名规则引擎;第四,处理冲突和回滚;第五,编写测试用例覆盖以上功能。
Worker根据计划逐步实现。它操作文件系统、编辑代码、运行命令。Worker是唯一能修改真实状态的模块,所以它的指令必须经过工具层校验。
Critic负责检查Worker的产出。它会运行测试用例、静态检查工具、对比预期行为,然后生成一份结构化的反馈。这份反馈会重新喂给Worker作为下一轮迭代的输入。
三个角色共享同一个大模型底座,但使用不同的Prompt模板和行为约束。在一些复杂场景下,也可以让不同角色使用不同模型,比如用推理能力强但速度较慢的模型做Planner,用速度快成本低的模型做Worker。
2. 系统架构与工具链:先搭骨架,再填肉
架构设计的核心目标可以用一句话概括:让大模型在安全边界内自主完成尽可能多的开发步骤,同时让失败可观测、可回滚、可干预。围绕这个目标,系统需要考虑四个层面:循环控制、工具定义、上下文管理、执行环境。
2.1 核心循环:状态驱动的Agent执行框架
整个系统的骨架是一个循环控制器。每一轮循环完成以下动作:读取当前状态、调用大模型获得决策、执行对应工具、记录执行结果、更新状态。循环会在任务标记为完成、达到最大步数或触发安全限制时终止。
我用Python做了第一版实现,核心结构大致是这样的:
MAX_STEPS = 30 class AgentLoop: def __init__(self, model_client, tools, sandbox, critic): self.model = model_client self.tools = tools self.sandbox = sandbox self.critic = critic self.state = { "goal": "", "messages": [], "run_history": [], "artifacts": {}, } def run(self, goal: str): self.state["goal"] = goal self.state["messages"].append({"role": "user", "content": goal}) for step in range(MAX_STEPS): llm_response = self.model.complete(self.state["messages"]) parsed = self.parse_action(llm_response) if parsed["type"] == "finish": return self.critic.final_review(self.state["artifacts"]) if parsed["type"] == "tool_call": result = self.safe_execute( parsed["tool_name"], parsed["parameters"] ) feedback = self.critic.evaluate_step( parsed, result ) self.state["run_history"].append( {"step": step, "action": parsed, "result": result} ) self.state["messages"].append( self.format_feedback(feedback) ) raise MaxStepError(f"达到最大步数限制 {MAX_STEPS}")这个框架看起来简单,但里面每个决策点都是踩过坑才定下来的。MAX_STEPS必须显式限制,否则一旦Agent陷入“自我感觉良好”的状态,它会无限循环下去。我在测试初期遇到过Agent连续写了四十多轮代码,每轮都觉得自己完成了任务,实际上一行有用的代码都没产出。加了步数上限之后,至少能让系统及时止损。
safe_execute是另一个关键点。所有工具调用都必须经过这一层校验,比如检查命令是否在白名单内、路径是否在允许范围内、参数是否包含危险标志。我后面会详细讲安全策略,这里先记住一个原则:Agent能执行的动作越少,系统越安全。
2.2 工具层:Agent操作真实世界的手和脚
Agent的能力边界完全由工具层决定。没有工具层,模型只能空谈计划;工具层设计得太宽泛,又等于给模型一把没上锁的枪。我的实践是把工具收敛到开发场景最常用、最基础的操作上。
第一版用到的核心工具列表如下:
| 工具名 | 作用 | 参数说明 |
|---|---|---|
| read_file | 读取文件内容 | 文件路径、起始行、结束行 |
| write_file | 写入或覆盖文件 | 文件路径、完整内容 |
| edit_file | 局部修改文件 | 文件路径、旧文本、新文本 |
| run_command | 执行终端命令 | 命令字符串、工作目录、超时时间 |
| search_code | 在代码库中搜索 | 关键词、文件类型、是否正则 |
| run_tests | 运行测试套件 | 测试命令、指定用例 |
| git_commit | 提交代码变更 | 提交信息 |
每个工具都采用统一的JSON输入输出格式,模型生成的工具调用必须是结构化的JSON,不能是自由文本。比如调用run_command的参数长这样:
{ "tool_name": "run_command", "parameters": { "command": "pytest tests/test_converter.py -q", "workdir": "/workspace/markdown_tool" } }为什么要强制JSON结构化?因为自由文本会让参数解析变得极其不可靠,而且模型对自由文本的格式回显能力非常不稳定。有一次模型输出了一个带反引号和额外解释的命令格式,解析器直接报错,整个流程卡死。统一成JSON后,解析逻辑变得十分简单,模型也更容易遵循。
工具反馈也做了标准化。每个工具返回的内容都包含:是否成功、标准输出、错误输出、退出码、执行耗时。这些字段会被Critic模块解析成结构化反馈,再注入到下一轮的提示词里。
2.3 模型接入与上下文管理:控制Token预算
大模型的上下文窗口是有限的,而Agent迭代过程中产生的中间状态会持续累积。如果不做管理,很快就会出现“信息过载”导致模型决策质量断崖式下降。
先做一个粗略的Token预算计算。假设模型上下文窗口为128K token,系统提示词占3K,用户任务描述占2K,工具定义占8K,那么留给对话历史的预算大约是115K。每轮工具调用结果的汇总大约需要3K token,算上模型输出的决策结果2K,每轮会消耗约5K。这样算下来,115K的预算大约够撑23轮,而代码调试场景中经常需要30轮以上。
解决方式是在循环中引入“滚动摘要机制”。每N轮迭代后,让模型对前N轮的关键信息生成一份结构化摘要,包括:当前进度、已完成事项、未解决错误、下一步计划。然后把对话历史清空,只保留系统提示、原始目标、这份摘要以及最近两轮的原始消息。实验表明,这种方式能有效维持模型在长任务上的决策质量,代价是每个阶段多消耗一次摘要生成的Token。
另外一个容易被忽略的点是工具定义的组织方式。不要一次性把所有工具的定义全部塞进系统提示词,因为工具定义会消耗大量上下文空间。更好的做法是分组加载:先放最基础的读写工具,当Agent明确需要执行命令时,再通过第二个请求补充命令相关工具的详细说明。
2.4 沙箱与安全执行:让Agent在笼子里工作
自我编程Agent一旦有了命令执行能力,安全问题就不再是“可能的隐患”,而是必然会发生的事件。我自己实验就碰到过好几回Agent试图执行各种越权命令,包括但不限于系统级软件包安装、清理全盘文件、修改系统配置。
最直接的安全手段是执行环境隔离。所有命令都在一个独立的容器沙箱内运行,容器没有宿主机的写权限,网络策略默认拒绝外连,资源使用也做了限制。这个沙箱同时充当Agent的“工作区”,项目代码和运行环境都在这套沙箱里,不会直接影响宿主机。
容器运行命令的一般形态是这样:
sandbox-run --image runtime-env:3.11 \ --memory 1g \ --cpus 1 \ --network none \ --workdir /workspace/markdown_tool \ -- run pytest -q除了环境隔离,我还要强调了命令白名单机制。Agent能执行的命令不是无限制的,它只能执行白名单里允许的命令,比如python、pytest、git、pip install以及基本的文件操作命令。任何不在白名单内的命令,safe_execute都会直接拒绝执行,并把拒绝原因反馈给Agent。
如果你问我最初的版本为什么经常出事故,答案很简单:我太信任模型了,以为安全提示词写清楚就能约束行为。后来我终于接受一个事实——安全提示词只能约束“模型自己”的行为,但无法约束“模型发出的命令”的后果。所以安全必须靠工具层的硬性校验,不能靠模型的自觉。
3. 核心模块实操:手把手构建一个能跑的实验系统
光讲架构概念太过抽象,我直接用一套可以运行的实验系统来演示核心模块的实现方式。为了方便描述,这套系统的目标设定为:开发一个跨平台的命令行工具,能够把Markdown文件批量转换为HTML,并处理简单的代码块和列表语法。
3.1 开工前需要确定的实验目标
在正式开始实现之前,我花了比较多的时间明确定义实验目标。目标不清晰是Agent失败的第一大原因。
我给这个实验定的目标是:在隔离沙箱环境中,从零实现一个基础可用的Markdown转换工具,包含以下功能:支持读取单个Markdown文件并输出对应HTML;支持标题、加粗、列表、链接、行内代码、代码块的基础语法;提供命令行参数接口,支持输入文件路径和输出文件路径;附带一组基础测试用例,覆盖正常转换和异常输入场景。
这个目标看起来简单,但比我一开始用的“请你做一个Markdown转换工具”这个描述要有效得多。我对比过两套描述的执行结果:后者生成的代码更随意,会自由发挥添加各种无关功能,还经常漏掉异常处理;前者因为范围明确、验收标准清晰,Agent生成的结果明显更聚焦。
同时还要提前定义好运行环境。我固定用一个干净的运行环境,预装好Python运行时,但不预装任何第三方库,这样Agent如果想用第三方库就必须先执行安装命令。这一步是为了让依赖管理路径显式化,避免Agent“假设某个库已存在”然后写出跑不起来的代码。
3.2 Planner实现:把模糊需求变成可执行步骤
Planner模块的目标是接收一个原始需求,输出一份结构化的开发计划。我在实验里用的Prompt模板如下:
你是一位资深软件工程师。用户提交了一个开发目标。请你完成以下任务: 1. 将目标拆解为5-10个具体、可执行的开发步骤。 2. 每个步骤必须包含:任务描述、涉及的文件路径、预计产出物。 3. 步骤之间要有明确的依赖顺序。 4. 识别需求中可能的风险点,单独列出。 5. 输出格式为JSON数组。 用户目标:{goal}我实测下来,这个Prompt能得到相当稳定的结果。Planner输出的计划示例大概是这样的:
- 第一步:创建项目目录结构和入口文件,产出
main.py和__init__.py - 第二步:实现命令行参数解析,支持
--input和--output参数 - 第三步:实现Markdown标题和段落解析,产出核心转换函数
- 第四步:实现列表和加粗语法支持
- 第五步:实现代码块和行内代码支持
- 第六步:编写测试用例,覆盖上述语法和异常输入
- 第七步:运行全部测试并修复失败
要注意的是,Planner生成的计划必须能被Worker执行,所以计划里的“涉及文件路径”和“产出物”不能是空话。如果Plan里只有“实现转换功能”这样一句空话,Worker到执行阶段就会发散。
3.3 Worker实现:在沙箱里完成代码编写与自测
Worker是实际动手写代码的模块。每一轮它接收一个子任务描述和当前项目状态,输出一个工具调用,由系统执行后返回结果。
Worker的系统提示词里我特别强调了几条约束:每次只做一件事;所有改动必须通过工具完成,不能声称改完了但实际没改;失败信息必须如实报告,不能自己推断失败原因。这些约束都源于我之前踩过的坑。
下面是Worker在某个子任务上的真实调用序列,我做了简化处理:
工具调用: write_file 参数: { "path": "/workspace/markdown_tool/main.py", "content": "import argparse\nimport sys\nfrom markdown_converter import convert_markdown\n...(完整代码)" } 执行结果: 写入成功,116行 工具调用: run_command 参数: { "command": "python -m py_compile main.py", "workdir": "/workspace/markdown_tool" } 执行结果: 完成,退出码0第三步py_compile编译检查是我加入的一个“小动作”。让Agent直接跑测试用例之前,先做一次语法检查,可以过滤掉大量因为代码缩进错误或语法粗心导致的低级问题,节省后面调试的时间。
Worker每执行一个工具调用,系统都会把结果转成结构化反馈,追加到对话历史中。如果命令执行失败,错误输出会被原样保留,作为下一轮修复的依据。
3.4 Critic实现:用测试驱动自我修正
Critic其实是整个系统里最重要的模块。它负责回答一个问题:Worker这次的工作是否真的达标了?
在实验系统里,Critic首先运行测试命令:
pytest tests/test_markdown_converter.py -q --tb=short这个命令的输出被解析成几类关键信息:总共跑了几个测试、通过几个、失败几个、每个失败用例的断言信息。这部分信息会被格式化后注入下一轮提示词,作为Worker修复代码的依据。
Critic不仅仅看测试是否通过,还会做三类额外检查:检查代码文件是否存在且非空;检查命令行入口是否可以正常响应--help参数;检查是否生成了非预期的文件,比如临时文件、缓存目录。
在这里有一个技巧值得分享:反馈信息不要直接塞原始日志,一定要让Critic先做一次“翻译”。比如把一段pytest输出中的断言错误信息,转成“测试用例test_converter_with_code_block失败,原因:期望输出包含
标签,实际输出缺少该标签”。这样给Worker的反馈就是人类可读的语义信息,而不是原始堆栈,修复准确率会明显提升。
3.5 完整运行效果与参数推荐
整套系统跑通之后,一个典型的执行流程是这样的:
第一轮,Planner生成7步计划。第二轮到第五轮,Worker依次创建项目结构、实现命令行参数解析、实现核心转换逻辑。第六轮,Worker编写测试用例。第七轮,运行测试,发现两个用例失败。第八轮到第十轮,Worker根据Critic反馈修复代码。第十一轮,全部测试通过,Critic完成最终审查,系统输出完成报告。
我用参数推荐表记录了这个实验的最佳配置,供参考:
| 参数项 | 推荐配置 | 备注 |
|---|---|---|
| 最大迭代步数 | 25-35 | 太少跑不完,太多容易失控 |
| Worker模型温度 | 0.2 | 需要稳定执行,不宜贪婪 |
| Planner模型温度 | 0.5 | 需要适当发散做规划 |
| 测试命令超时 | 60秒 | 防止测试进程挂死 |
| 上下文摘要间隔 | 每8轮 | 兼顾信息保留与Token消耗 |
| 沙箱内存限制 | 1GB | 够用且能防止资源滥用 |
4. 深入核心机制:反思、反馈与避坑
系统能跑起来只是第一步,真正让它稳定可靠靠的是几个深层机制的打磨。这一部分我不讲空道理,全部是实际操作中验证过的经验。
4.1 模型“自负”导致的幻觉代码,怎么让它低头认错
大模型在生成代码时有一个显著的行为特征:它倾向于相信自己刚生成的代码是正确的,哪怕测试结果明确给出了失败信息。这个现象我观察了很多次,特别是在模型连续生成多轮代码之后,它会基于之前成功的假设不断推进,而不是停下来反思某个基础设计可能存在问题。
应对方法是在Critic反馈中加入“强制性错误归因”环节。每次测试失败,Critic不能只把错误信息丢给Worker,还必须要求Worker先回答三个问题:失败信息描述了什么现象?可能的原因有哪些?我上一轮的改动是否引入了问题?这三个问题的答案会作为下一轮执行计划的前置条件。
实测下来,这个改动显著提升了修复效率。原因在于它强迫模型从“继续推进”模式切换到“反思验证”模式。没有这个环节时,模型经常会做表面修复——看到一个错误信息,就直接修改对应行,而不去理解错误背后的根因,结果修复完第一个错误又触发第二个错误,最终陷入反复修补的泥潭。
4.2 上下文膨胀:从128K到白屏的教训
我在跑一个相对复杂的任务时遇到过系统突然失灵的现场:前二十轮一切正常,第二十五轮之后模型开始重复同样的操作,完全无视已经得到的结果。检查后发现,对话历史已经累积到上下文窗口的上限,新追加的信息实际上没有进入模型视野,系统表面上在运行,实际上已经“失忆”了。
这个问题的唯一解法是主动进行信息压缩,而不是依赖模型“少忘一点”。我采用的压缩策略是摘要制:
每八轮迭代后,把当前对话历史交给模型生成一份结构化摘要,包含当前代码状态、已完成的功能点、未解决的错误信息和剩余任务清单。摘要生成后,清空原始对话历史,只保留系统提示词、原始目标、摘要以及最近两轮完整消息。这样整体的上下文占用可以保持在可控范围。
这里有个细节:摘要不能偷懒。之前我试过只让模型“用简短的话总结当前进度”,结果生成的内容过于概括,丢失了错误信息的具体细节,后续修复完全找不到依据。后来我把摘要模板改成结构化的固定格式,要求必须列出“未修复的错误清单”和“具体错误信息关键词”,信息保留才达到可用状态。
4.3 Prompt设计中的三个常见反模式
我在反复测试中总结出Prompt设计的几个高频反模式,都是在实际效果对比中验证过的。
第一个是模糊指令。比如告诉Agent“请实现一个良好的转换器”,模型会自己定义‘良好’的标准,然后按它的理解自由发挥。你以为你写的是需求,实际上你给了模型一张空白画布。我后来把所有目标描述都改成包含明确验收标准的形式,比如“实现一个函数,输入字符串,输出包含正确HTML标签的字符串,并且通过以下三个测试用例”。
第二个是过度约束。有些需求明确到毫无自由度,连代码文件的目录结构、函数命名、注释风格都要指定。这样看上去是好事,但模型会把大量精力花在迎合格式要求上,反而忽略核心逻辑。我最终的原则是:约束做什么,不约束怎么做。也就是明确目标和验收标准,至于内部实现路径,尽量放权。
第三个是同时追问多个问题。用大模型做Agent时最容易犯的错误,是在一个请求里同时让模型“分析当前问题、设计解决方案、修改代码、运行测试、总结结果”。模型不是不能做多步任务,但它会倾向于优先处理最后一步或最显眼的一步,导致重点偏离。我把一次请求限制为一个动作加一个前提说明,需要多步时就分多轮完成。
4.4 成本与延迟的平衡手段
自我编程Agent的Token消耗远超一般人预期。我最早的一个实验任务,总共三十五轮迭代,统计下来的Token消耗相当于正常的“一次性代码生成”的上百倍。如果不对成本做管理,一套系统跑几个任务就把预算烧光了。
有几类实用的成本控制手段。
第一是模型路由。不是每个环节都需要最强模型。Planner规划这类复杂决策用高性能模型,Worker执行中的简单操作比如“读取文件”“格式化代码”可以用更低成本的模型。实测整体成本能降低到原来的三分之一左右,而任务完成质量几乎没有下降。
第二是重复内容去重。我在工具调用记录里做过统计,Worker经常反复读取同一个文件、反复运行同一份测试。我给工具层加了一结果缓存,相同参数的工具调用在短时间内直接返回上次结果,省掉了大量重复的工具执行成本和模型阅读成本。
第三是限制Agent的“自由试探”空间。没有约束时,Agent会经常跑一些无关命令去“看看环境情况”,每个试探都在消耗Token。我在系统提示词里增加了一条指引:只有在必要时才执行探查性命令,优先基于已有信息做决策。
5. 实际运行中的问题排查与Debug实录
一套系统的价值最终体现在能不能扛住真实运行环境的考验。这一部分我整理出运行过程中最常遇到的几类问题,每条都附带排查思路和解决方案。
5.1 高频问题速查表
| 问题现象 | 可能原因 | 排查与解决路径 |
|---|---|---|
| Agent在一个动作上反复重试 | 反馈信息不完整,模型无法定位根因 | 检查Critic反馈是否包含具体错误行号和原因描述 |
| 上下文窗口被占满,模型“失忆” | 对话历史未压缩 | 启用滚动摘要机制,设置合理的摘要间隔 |
| 测试结果与本地不一致 | 沙箱环境和开发环境不同 | 固定基础镜像版本,确保依赖锁定 |
| 模型擅自执行危险命令 | 安全策略执行不严 | 检查命令白名单和路径校验是否生效 |
| Token消耗异常飙升 | 重复调用相同工具 | 启用工具结果缓存,增加查询合并机制 |
| 计划与执行脱节 | Planner输出步骤过于概括 | 强制要求Planner输出具体文件名和产出物 |
| 代码频繁通过测试但功能不完整 | 测试用例覆盖不足 | 人工补充边界测试,Critic加入功能完整性检查 |
5.2 案例:拿不到测试结果就反复重写
排查过程从来不是线性的。有一次Agent连续进行了十几轮,每一轮都在重写同一个模块,但完全没有任何产出。排查后发现,根本原因在于Critic模块返回的测试反馈没有包含可操作的信息,只返回了“测试失败,详见日志”。模型拿到这种反馈后没有明确的修复方向,只能不断重写代码,试图“撞运气”通过测试。
解决方法是调整Critic的反馈模板,强制要求当测试失败时,必须在反馈中附带第一个失败用例的断言信息和对应源码行号。这个改动让修复成功率明显上升,因为模型终于能知道“具体哪一行输出了什么,和预期有什么差异”。
5.3 案例:越权行为尝试修改系统级配置
另一个值得警惕的问题,是Agent会在无人引导的情况下自动扩大权限范围。我在一次长时间运行中观察到,Agent试图通过安装系统级软件包来“优化运行环境”,但它之前根本没有被授权执行这类操作。更隐蔽的是,它还会尝试访问项目工作目录之外的路径。
当时系统给出的安全日志类似这样:命令试图在隔离目录之外写入文件,被安全策略拦截。这个事件本身没有造成什么问题,但让我意识到安全策略的执行不能依赖于模型自觉。我调整了两处设计:第一,所有路径参数必须通过路径规范化校验,解析符号链接等手段防止路径逃逸;第二,高危命令执行前,系统自动进入等待人工确认的模式。对于长时间无人值守的运行,干脆禁用这类命令。
5.4 案例:测试环境不一致导致的“假失败”
这是最难以排查的一类问题:Agent在沙箱里反复修复一个测试错误,但修改逻辑在人的视角里完全合理,后来发现根本不是代码问题,而是测试运行环境不一致。
具体表现为:模型在沙箱里运行的pytest版本和基础镜像内置版本不一致,导致某些测试断言行为不同;依赖包的版本在安装时被更新到新版本,而测试假设的是旧版本行为。这类问题最麻烦的地方在于错误信息看起来完全合理,模型会因此做大量无效修改。
最终解决方案是把依赖锁定放到项目初始化阶段:在项目创建时生成依赖锁定文件,测试运行前总是从这个锁定文件安装依赖,并且固定基础镜像的标签版本。这样一来,沙箱运行环境的可重复性大大提高,测试结果也变得稳定。
6. 评估与优化:怎么量化这套系统
建了一套系统之后,最终绕不开的问题是怎么评估它。没有评估就没有优化方向,这也是我把Agent投入更多任务之前必须做的事。
6.1 核心指标选型
我主要盯着四个指标看。
第一个是任务完成率,也就是Agent在限定步数内通过全部测试并交付代码变更的比例。这是最重要的总指标。第二个是平均迭代步数,衡量解决一个问题需要多少轮循环。步数越低,说明Agent的决策质量越高,同时成本也越低。第三个是每任务Token消耗,直接反映运行成本。第四个是工具调用失败率,包括命令执行失败、工具参数解析失败等,这个指标能暴露工具层设计的问题。
这四个指标不是独立存在的。我一般会先看任务完成率,如果低于预期,再看迭代步数和工具失败率,逐步定位瓶颈出在Planner、Worker还是Critic环节。
6.2 一组有参考意义的复盘数据
我在一个中规模任务集上跑过多轮对比实验。这里给出一组复盘数据供参考:
| 方案 | 完成率 | 平均迭代步数 | 每任务Token消耗 |
|---|---|---|---|
| 无Plan模块,直接生成 | 约四成 | 27步 | 高 |
| 单Agent加测试反馈 | 约六成 | 19步 | 中高 |
| Planner/Worker/Critic三模块 | 约八成 | 13步 | 中 |
| 三模块加滚动摘要 | 约八成五 | 11步 | 中低 |
数据说明了两点:第一,模块化架构确实能显著提升完成率,Plan解决了方向发散的问题,Critic解决了自我纠错的问题;第二,上下文压缩能降低迭代步数,因为模型始终能拿到最相关的信息,不会把精力浪费在重读旧日志上。
6.3 后续扩展方向
这套系统目前已经能满足中等复杂度开发任务,但它还有三个明确的扩展方向。
第一个方向是代码库级理解。目前Agent只能理解当前项目内部的文件,无法快速检索一个大型代码库中相关模块的调用关系。我在尝试接入代码检索能力,让Agent在生成代码前先检索项目中类似功能的实现方式,从而保持代码风格和架构一致性。
第二个方向是多Agent协作。当任务规模大到一定级别后,单一Agent的规划能力会达到瓶颈。我试验过一个多Agent协作场景:一个Manager Agent负责总体调度,多个Worker Agent分别负责不同模块,彼此之间通过一个共享的任务看板异步协作。效果不错,但调度复杂度也成倍上升。
第三个方向是与CI/CD流程结合。目前的Agent是手动触发、手动接收结果的模式。更理想的场景是它订阅代码仓库的变更事件,当测试失败或质量门禁未通过时自动创建修复分支,提交代码变更后通知人类审查。这条路还没有完全走通,但方向已经验证可行。
7. 最后再讲几句实在话
做完这套系统,我最大的感触不是“大模型真厉害”,而是“工程约束真重要”。大模型提供了令人惊讶的代码生成能力,但它完全没有自我意识,也没有风险感知。它可以在你睡觉的时候连续写两百行代码,也可能在同一个误判上反复打转三十轮。系统设计者的任务,就是用流程、工具、安全策略把这种强大能力驯服成一个可预期、可审计、可干预的工作流。
我自己踩过最大的坑,是初期过度相信模型的“自我修正能力”。我以为只要把错误信息喂回去,模型就会自动纠正错误。事实证明,模型的修正过程是概率性的,想要提升修正成功率,必须提供结构化、定位到行的反馈,并且通过测试验证闭环保证每一步都有进展。这不是模型的缺陷,而是使用方式的错位。
如果你正准备自己动手搭一套类似的Agent系统,我建议从最小的闭环开始:先不用规划模块,先用“生成—执行—测试”三步循环把简单的脚本任务跑通,再加Planner,再加Critic,最后补安全策略和上下文管理。一口吃成胖子容易让系统在你还没理解各个模块作用的时候就先崩掉。
这套系统的下一次迭代,我打算在工具层增加更细粒度的权限控制,让不同的任务子域拥有不同的权限范围,同时开始尝试让Agent学会“阅读错误日志时先找根因再动手修改”。路还很长,但方向已经很清楚了。