ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建 Coding Agent:ReAct 循环、工具设计与实践踩坑全解析

从零搭建 Coding Agent:ReAct 循环、工具设计与实践踩坑全解析 1. 先认清黑箱里到底有什么Coding Agent 的最小组成很多人第一次接触 Coding Agent都会产生一种感觉这东西像个黑箱丢一个需求进去它自己读代码、改文件、跑测试最后吐出一个 PR。中间到底发生了什么谁也说不清。我刚开始也是这么想的直到自己动手搭了一个才发现所谓的黑箱拆开看就是几个非常朴素的组件只是被包装得看起来很高端。1.1 一次 LLM 调用和一个 Agent 的本质区别先说一个最容易被绕晕的点。普通 LLM 调用比如你问 GPT帮我看看这段代码有什么问题是一次性的输入一个 prompt输出一段文本完事。就算你让它再想想它也没有能力自己打开你的项目文件、实际跑一遍测试、根据报错改代码它只能基于你塞进上下文里的信息继续编。Coding Agent 不一样的地方在于它把那次性的调用变成了一个循环模型输出一个意图比如我想读某个文件程序替它执行这个动作把执行结果文件内容、测试输出再塞回给模型模型根据新信息决定下一步动作。这个循环一直转直到模型认为任务完成了。所以 Agent 的本质不是更聪明的 LLM而是LLM 一套能动手的工具 一个替它反复决策的循环。理解这一点后面所有设计都有了着落。1.2 四个核心部件模型、工具、循环、记忆我搭完第一个可用版本后回头总结一个 Coding Agent 跑起来只需要四样东西模型负责推理和决策也就是大脑。它不需要真的会写代码它只需要知道该调什么工具、该看什么信息。工具模型实际动手的手脚。对编程场景来说最基础的三件就是读文件、写文件、执行命令。循环把模型输出转成工具调用、把工具结果喂回模型、判断何时终止。这是 Agent 的脉搏。记忆这里特指消息历史。模型本身没有状态所有已经读到的信息、已经做的修改、测试结果都得靠消息记录保存在上下文里模型才能记得自己刚才干了什么。下面我就拿 Python 和 OpenAI 的函数调用协议从零把这个骨架搭出来。整个代码量不大跑通之后你会觉得哦原来所谓 Agent就是一层循环加几个函数压根没有魔法。2. 从零搭骨架Python 实现 ReAct 循环ReActReasoning Acting是目前绝大多数 Agent 的基础范式模型先推理再行动然后根据观察到的结果继续推理。这个模式在 Coding Agent 里格外好用因为写代码本身就是一个试错过程。2.1 用函数调用协议声明工具能力现在主流的大模型 API 都支持 function calling也就是你在请求里声明一批工具的 JSON Schema模型在需要的时候会返回一个结构化指令告诉你我要调用哪个工具、传什么参数。这是 Agent 的标准电压比早期那种让模型输出特定文本再正则解析的方式可靠得多。先定义三个最基础的工具TOOLS [ { type: function, function: { name: read_file, description: 读取指定路径的文件内容返回文件文本。如果文件较大只返回前 200 行。, parameters: { type: object, properties: { path: {type: string, description: 相对于项目根目录的文件路径} }, required: [path] } } }, { type: function, function: { name: write_file, description: 将完整内容写入指定文件会覆盖原有内容。写入前请确保已经 read_file 看过当前内容。, parameters: { type: object, properties: { path: {type: string, description: 相对于项目根目录的文件路径}, content: {type: string, description: 要写入的完整文件内容} }, required: [path, content] } } }, { type: function, function: { name: run_command, description: 在项目根目录执行 shell 命令返回标准输出和标准错误。适合运行 pytest、python 脚本等。, parameters: { type: object, properties: { command: {type: string, description: 要执行的 shell 命令} }, required: [command] } } } ]注意我在 description 里写了不少约束比如 read_file 说只返回前 200 行write_file 说写入前必须先看原文件。这些约束不是废话模型真的会读这些描述来约束自己的行为。工具描述写得越清楚Agent 的精神状态就越稳定。2.2 execute_tool 分发器与循环主体工具声明好了接下来写一个分发函数把模型的工具调用请求转成真实的 Python 函数执行import subprocess import json from pathlib import Path ROOT Path(/path/to/your/project) def execute_tool(tool_call): name tool_call.function.name args json.loads(tool_call.function.arguments) if name read_file: path ROOT / args[path] if not path.exists(): return f错误文件不存在 {path} lines path.read_text(encodingutf-8).splitlines() body \n.join(lines[:200]) if len(lines) 200: body f\n... (共 {len(lines)} 行已截断) return body if name write_file: path ROOT / args[path] path.parent.mkdir(parentsTrue, exist_okTrue) path.write_text(args[content], encodingutf-8) return f已写入 {path}共 {len(args[content])} 字符 if name run_command: result subprocess.run( args[command], shellTrue, cwdROOT, capture_outputTrue, textTrue, timeout30 ) output result.stdout \n result.stderr # 截断超长输出防止上下文爆炸 if len(output) 4000: output output[-4000:] return output return f未知工具: {name}然后是整个 Agent 的心脏——循环主体from openai import OpenAI client OpenAI() SYSTEM_PROMPT 你是一个运行在本地代码仓库中的 AI 编程助手。……见第 4 节 def run_agent(task: str, max_iter15) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task}, ] for step in range(max_iter): response client.chat.completions.create( modelgpt-4o, messagesmessages, toolsTOOLS, tool_choiceauto, temperature0.2, ) msg response.choices[0].message messages.append(msg) # 没有工具调用说明模型觉得任务结束了 if not msg.tool_calls: return msg.content # 逐个执行工具调用把结果追加进消息历史 for tool_call in msg.tool_calls: result execute_tool(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: result, }) return 达到最大迭代次数任务未完成到这里一个能跑的 Agent 已经有了。它拿到任务后会自己读文件、改代码、跑测试然后根据测试结果继续修直到所有测试通过或者迭代次数耗尽。2.3 我把循环参数调成了什么值几个参数我实际调过的经验值直接给你参考参数我用在 Demo 上的值说明max_iter15太少容易完不成任务太多会累积大量 token 成本temperature0.2编程任务要确定性不能用默认的 0.7 或 1.0工具输出截断4000 字符测试日志动辄几千行必须截断否则上下文很快炸command 超时30 秒防止模型写出死循环命令或卡住的测试15 次迭代听起来不多但每次迭代里模型可能一次调用多个工具实际能完成的操作远超 15 次。以修 bug 的场景来说一个典型的任务大概会经历读 2~3 个文件、改 1 次代码、跑 2~3 次测试差不多 6~8 个工具调用所以 15 次迭代是够用的。3. 工具设计是能不能用的分水岭同样一个 LLM工具设计得好不好决定了 Agent 是聪明的实习生还是只会复读的聊天机器人。这一节说说我在工具粒度上的取舍。3.1 read_file / write_file / run_command 的粒度取舍一开始我贪多给 Agent 准备了很多工具search_symbol、find_file、list_directory、edit_line、insert_code……结果模型在选择工具上浪费了大量决策而且经常选错。后来我砍到只剩三个工具效果反而好了。原因很简单工具越少模型的决策负担越小每个工具被调用的频率越高模型对工具行为的预期越准确。read_file承担所有看的需求包括看目录结构也行直接读目录会返回错误或列表模型能接受。write_file承担所有改的需求用整文件覆盖而不是行级编辑。行级编辑看起来省 token但要求模型精确计算行号一旦文件被并发修改就全乱了。整文件覆盖虽然每次可能多传几百行但对于小项目来说完全够用而且逻辑简单、不容易出错。run_command承担所有验证的需求。模型写完代码自己跑 pytest看到失败信息再改这是 Coding Agent 区别于代码生成器的核心。3.2 工具执行结果的截断与格式化工具返回的结果最终都要拼进 messages 里重新发给模型。这段内容的质量直接决定了模型的判断。我有两个习惯。第一所有输出都截断read_file 截到 200 行run_command 截到 4000 字符。截断比不截断好因为满屏的日志反而会让模型抓不住重点。第二在结果里加上必要的元信息比如文件路径、总行数、字符数。这些信息帮助模型建立对项目的空间感。有个反直觉的发现在工具结果前面加一个简短的状态描述比如命令执行成功但测试有 3 个失败项会让模型的理解准确很多。因为模型是文本推理的给它一个摘要 原始输出的格式相当于给它配了个前额叶。3.3 为什么我没让 Agent 直接用终端跑任意命令有些项目会让 Agent 直接连上终端想跑什么跑什么。我明确不这么做至少在第一版不这么做。原因不是技术上的而是安全上的模型可能跑出rm -rf也可能因为一个拼写错误把环境搞坏。折中方案是把 run_command 做成一个白名单命令转发器只放行安全的指令前缀SAFE_PREFIXES (pytest, python, ls, cat, grep, git diff, pip install -r) def run_command(command: str) - str: if not any(command.startswith(p) for p in SAFE_PREFIXES): return f错误命令 {command} 不在白名单中只允许执行 {SAFE_PREFIXES} ...这样 Agent 依然能运行测试、执行脚本但没法做太出格的事。等到你对 Agent 的信任度上来了再逐步放宽比如加一个人工确认机制放行任何命令前弹个确认框。Demo 阶段白名单就够了。4. 系统提示词和参数最容易抄错的地方很多教程把这部分一笔带过但我实测下来系统提示词对 Agent 行为的影响不亚于工具设计。同一个模型、同一套工具换个提示词成功率能从 40% 提到 85%。4.1 我给系统提示词定的五条铁律系统提示词我最终收敛成了五条规则。每条都是踩过坑之后才加的你是一个运行在本地代码仓库中的 AI 编程助手。你的任务是帮助用户完成代码修改和问题修复。 工作流程 1. 先使用 read_file 阅读相关代码理解项目结构和现有逻辑 2. 定位问题想清楚修改方案后再动手 3. 使用 write_file 修改代码修改后立即用 run_command 运行测试 4. 根据测试输出判断是否完成测试失败则继续修复 铁律 - 修改任何文件前必须先 read_file 查看当前内容 - 一次只修改一个文件改完立刻验证 - 不要修改与任务无关的代码 - 如果连续两次修改都没有通过测试停止修改重新 read_file 阅读代码再思考 - 任务完成时用简短的中文汇报你做了什么修改、测试是否通过第五条连续两次失败就停下来重新读代码是解决死循环的关键。没有这条的时候模型会像一个倔强的实习生同一个方案改了三次还继续改。加上这条之后它会主动退回一步去看看是不是自己理解错了代码。4.2 温度、迭代上限、上下文窗口的搭配模型参数这块我的建议是温度一定要往低调。我用 0.2有人用 0效果都不错。写代码和写诗不一样不需要发散需要的是稳定。迭代上限上面说的是 15但这个值要配合上下文窗口看。gpt-4o 的 128k 窗口看着很大但每次工具调用结果都累积在消息里几轮下来就占掉一半。如果任务本身很大比如跨多个文件的重构就要么提高 max_iter要么做上下文裁剪二选一不能两个都省。我的判断标准是如果 Agent 在 10 次迭代内还没有迈出第一步比如一直读文件、没写过任何代码大概率是它卡住了这时候调大迭代次数没有意义应该去查是工具定义有问题还是提示词让它困惑了。5. 实测中的三个翻车现场与修复过程光讲原理不够我把实际跑的时候遇到的三个最典型的翻车场景拉出来每个都是卡死级别的而且都有通用的解法。5.1 翻车一同一个错误反复修不好陷入死循环场景我让它修一个 Python 脚本里的逻辑 bug。Agent 第一次改完pytest 报AssertionError它看了一眼错误信息又改了同一行还是同样的报错第三次又改了同一行依然报错。直到 15 次迭代耗尽。我分析日志后发现问题出在报错信息给的信息太局部了只报assert result 5失败但 Agent 不知道result是怎么算出来的。它每次都在猜而且每次都猜同一个方向。修复办法有两个我都用了一是系统提示词里的连续两次失败就重新读代码。 二是给 run_command 结果加一条后处理逻辑如果测试失败自动把测试文件内容附在输出后面让模型看到测试到底在测什么。if FAILED in output and test in args[command]: # 找到测试文件读出来放在报错后面 ...加了第二个措施之后Agent 不再瞎猜了它能看到测试的断言逻辑理解目标值是怎么来的修复准确率明显上升。5.2 翻车二读文件太多上下文窗口溢出场景任务涉及一个多文件项目Agent 为了搞清逻辑一口气 read_file 了七八个文件每个文件几百行。到了第五轮迭代API 直接返回错误说消息长度超过上下文限制。这个问题的本质是Agent 没有遗忘机制。所有读过的内容都在消息历史里模型只能被动接受。我的解法是做一个简单粗暴的滑动窗口def trim_messages(messages, max_len30000): total sum(len(m.get(content, )) for m in messages) if total max_len: return messages # 保留 system 和最近的若干条消息 head messages[:2] # system 原始任务 tail messages[-10:] # 最近 10 条含最新工具结果 return head [{role: system, content: 注意部分较早的对话内容已被截断请基于现有信息继续。}] tail滑动窗口不是完美的方案因为它会丢失早先读到的文件内容模型可能失忆。但在 Demo 阶段这是性价比最高的方案。真要做得优雅得引入摘要记忆把读过的文件内容压缩成要点存起来需要时再恢复这就属于进阶优化了。5.3 翻车三工具调用 JSON 解析失败导致中断场景某一次运行模型返回的tool_calls里某个参数的 JSON 出现了畸形json.loads直接抛异常Agent 当场崩溃。这类问题在小模型上尤其常见但是用大模型也可能偶发比如参数值里包含特殊字符时。我在分发器里加了容错try: args json.loads(tool_call.function.arguments) except json.JSONDecodeError as e: return f工具参数 JSON 解析失败: {e}。请重新检查参数格式。关键在于解析失败时不要把错误变成异常抛出而是把错误信息作为工具执行结果返回给模型。这个错误会进到下一轮消息里模型看到后会自己修正格式重新生成一个合法的调用。实测下来90% 的情况模型都能在下一轮自我纠正。这是 Agent 容错设计的通用思路让错误成为观察的一部分而不是中断循环的理由。6. 从能跑到敢用安全边界和进阶方向最后聊点务实的。Demo 跑通是一回事真正敢让它在你项目里干活是另一回事。安全边界和进阶能力是两条绕不开的路。6.1 目录白名单、命令黑名单与人工确认机制我的第一版 Agent 是往项目根目录一扔让它自由活动。后来我意识到一旦 Agent 能写文件、能执行命令它就有能力破坏你的开发环境。所以安全这块我建议按三层来搭路径层write_file 写入前校验路径是否在项目根目录内想写出去直接拒绝并返回错误信息。比如有人让它把日志写到 /tmp 或者家目录直接拦截。命令层上面提到的白名单前缀机制。另外对rm、sudo、curl | sh这类高危命令无条件拒绝。人工确认层对写操作弹确认框尤其是覆盖已有文件和执行非白名单命令时。这一步会打断 Agent 的自动化但它能让你在 Agent 犯大错之前踩住刹车。你自己跑着玩的时候可以不开真要对正式仓库动手必须开。这三层做完Agent 就像一个戴着镣铐的实习生能干很多活但干不出太出格的事。6.2 进阶路线记忆、规划器、多智能体协作最后一个路线图给想继续深入的人指个方向按难度排序摘要记忆工具结果太长时用一次额外的 LLM 调用把结果压缩成结构化摘要再存进上下文。这比粗暴截断聪明得多。规划器让一个高配模型先拆解任务生成一个带检查点的计划然后由执行模型按计划逐步执行。相当于给 Agent 配了个 Project Manager。DeepResearch 类产品基本就是这个思路。多工具并行模型一次可以发多个工具调用先并行执行再统一汇总能显著减少往返次数。多智能体协作一个分析员角色只读代码、出诊断报告另一个程序员角色只负责改代码还有一个评审员角色专门挑毛病。角色分离能让每个模型的上下文更干净出错率更低。我在实际使用中的体会是Coding Agent 的每一次智能表现背后都是工程设计的功劳。所谓黑箱其实是你没看见中间那层循环和工具。把它拆开、搭一遍、跑一遍、修一遍你对 Agent 的理解会比看十篇综述都深。这个从零搭建的过程本身就是最好的学习方式。
返回列表