
1. 先拆开黑箱Coding Agent 在循环里到底做了什么先抛出我的结论所谓 Coding Agent本质上就是一个“能连续调用工具的模型驾驶循环”。它没有隐藏的灵魂也没有神秘的代码生成引擎就是把传统上由人肉完成的一套动作——看代码、查错误、改动、跑测试、再检查——拆成模型能自主完成的若干步骤。很多人第一次接触 Agent 项目时会觉得它们玄乎是因为拿它和普通 LLM 应用比较总觉得“多了一层东西”。但拆开看多出来的部分其实非常朴素模型不再是“回答一次就结束”而是被放进了一个 while 循环里每次根据当前状态决定下一步调哪个工具工具返回结果后模型继续思考直到任务完成或达到步数上限。这套循环能够成立依赖的是 LLM 的推理能力和 function calling 能力。模型负责“思考”工具负责“动手”而 Agent 框架负责把两者粘起来。所以如果你要亲手构建一个 Coding Agent最核心的工作不是提示词写得多花哨而是把以下三件事做好定义清楚工具、维护好上下文、控制循环的边界。工具定义的意义在于给模型一个稳定且可预测的接口上下文管理的意义在于让模型始终知道当前代码库是什么状态循环控制的意義则在于防止模型在一个错误方向上越走越远。这三件事做扎实了即便用一个参数不大的本地模型也能跑出一个“看起来挺聪明”的编程助手。我经常用“带实习生的过程”来类比 Coding Agent 的工作方式。你给一名实习生一个任务比如“把登录接口的超时时间改成 30 秒”他不会直接改文件他会先打开项目目录看看结构找到登录相关的文件读一遍相关代码确认超时时间写在哪个常量里然后动手改改完跑一下测试最后把 diff 给你看。Coding Agent 的循环就是在模仿这个过程它有自己的“读文件工具”代替眼睛有自己的“命令行工具”代替手有自己的“测试命令”代替验证动作而每一次动作之后它都需要重新评估“当前和目标还差多少”。这样想的话整个黑箱就透明了剩下的问题只是工程实现层面的细节。不过有一点需要提前说清楚Coding Agent 并不是万能的。它擅长的是有明确入口、有可验证反馈的任务比如修 bug、补单测、做小型重构它不擅长的是需求本身模糊、验证标准也模糊的任务比如“把用户体验做得更好”。理解这一点能避免你在后续调试中产生不切实际的预期。1.1 一个任务从用户输入到可执行工具的链路我们用一个具体例子走一遍链路。假设我启动 Coding Agent 时输入的是这样一句话帮我把src/auth/login.py里的超时时间从 10 秒改成 30 秒并确保测试通过。这个输入首先会被放入消息列表作为用户的指令发送给模型。模型收到指令后并不直接改文件因为它没有改文件的能力它只能返回一个“工具调用请求”。比如它会说我需要调用view_file来查看src/auth/login.py的内容。Agent 框架收到这个请求后把它翻译成一个真实的 Python 函数调用执行读取然后把文件内容作为 tool 消息放回消息列表。模型看到文件内容后继续判断找到了超时相关的赋值现在需要调用edit_file把数值从 10 改为 30。框架再次执行编辑操作把结果返回给模型。模型再判断改动已经完成下一步应该调用run_command执行测试。测试返回通过后模型不再调用工具而是输出一段总结比如“已在 login.py 中修改超时时间所有测试通过”。此时循环结束用户拿到的就是一段可审查的说明以及实际落盘的改动。这就是整条链路。它并不复杂但每个环节都需要精心设计。比如view_file该返回整个文件还是按行号分段返回这会影响模型的上下文占用edit_file是接受“旧内容-新内容”还是“行号-新内容”会影响编辑的准确率run_command该给多少超时时间决定了编译类命令能不能完整执行。这些细节堆在一起就是 Coding Agent 实际工程体验的天壤之别。1.2 关键设计取舍为什么不能只靠“一次性提示”很多人会问为什么要搞一个循环不能把整个代码库塞进一次提示让模型一次性输出最终结果吗答案很现实代码库太大塞不下而且即使塞得下模型一次输出长代码的准确率也不够。更重要的是编程任务天然是“分步逼近”的不先看文件你不知道怎么改不看测试结果你不知道改对了没有。这种依赖后续观察反馈的任务只有允许模型在“行动—观察—再行动”的循环里推进才能做得稳。所以 Coding Agent 的第一设计原则不是“让模型更聪明”而是“让模型每次只做一个小决策但能快速从环境里获得反馈”。把小决策串起来最终就能完成一个大的目标。这也是为什么工具设计比提示词设计更关键工具就是模型与代码库之间的传感器和执行器传感器不清晰模型就会瞎猜执行器不安全模型就会闯祸。2. 最小可用的工具集怎么定五个工具就够跑通第一版很多新手在做 Coding Agent 时第一反应是“工具要越多越好”最好把代码搜索、git 操作、依赖安装、容器执行全都接上。我的建议恰恰相反第一版只需要五个工具跑通闭环之后再往上面加。工具越多模型的决策空间越大出错率越高调试成本也越高。一个理想的第一版工具集应当满足“能看、能改、能查、能跑、能收尾”五件事。我设计的第一版工具集是这样的view_file用于查看文件指定行范围的内容解决“看代码”的需求grep_search用于在仓库里做简单关键词搜索解决“找代码位置”的需求edit_file用于执行文本替换解决“改代码”的需求run_command用于执行终端命令解决“跑测试、看日志、查状态”的需求finish_task用于让模型主动结束任务并给出总结解决“收尾”的需求。这五个工具覆盖了一个最简单的编程闭环定位问题、理解代码、修改代码、验证结果、汇报结论。至于为什么要用finish_task这样一个显式的工具而不是靠模型自然输出结束是因为在复杂任务里模型很容易在一个子任务完成后继续做无关的修改。有了finish_task相当于给了模型一个“刹车”它能明确地宣布“我已经做完不再动代码”。在循环逻辑里只要检测到模型调用finish_task就立即停止循环把后续行为交给用户 review。2.1 工具不是越多越好先看“闭环”缺什么在给工具集做加法之前先做一个“闭环测试”拿到一个典型需求手工走一遍看哪一步当前工具集无法覆盖。比如需求是“修复某个测试失败”正常流程是先跑一次测试看报错再根据报错搜索相关代码查看代码后修改再跑测试验证。这正好对应run_command、grep_search、view_file、edit_file、run_command。如果需求是“给新模块加一个接口”那你可能还要补充“文件创建”能力这其实是edit_file里传入空 old_string 或单独一个create_file工具。我见过有人第一版就接了十几个工具结果模型频繁在工具之间跳来跳去反而把任务带偏。原因是模型并不是越多的选择越聪明而是越多的选择越容易误判。工具的 description 写得再清楚模型也有可能混用。所以工具设计的核心原则是每个工具的目的边界要清晰参数要少返回结果要有结构化字段。宁可多写几个工具函数也不要搞一个“万能执行器”。2.2 工具的安全边界命令白名单和超时控制run_command是 Coding Agent 里最强大也最危险的工具。一个能自由执行 shell 命令的 Agent如果跑在本地仓库上可能因为模型误判或工具 bug 造成严重后果。我的第一版实现里给run_command加了两道保险命令前缀白名单和超时控制。白名单里只允许pwd、ls、find、grep、cat、python、pytest、git status、git diff这几类命令超时统一设为 30 秒超过就杀掉子进程并返回 timeout 信息。其实更严谨的做法是直接用 Docker 容器跑整个 Agent让它在隔离环境里操作但这会让工具定义复杂一个量级不适合作为第一个版本。第一版求的是“能安全地在本地小仓库上跑通”所以用白名单和超时先兜住风险。如果你打算上生产再考虑容器隔离、非 root 用户、资源配额等手段。3. 从零实现你的第一个 Coding Agent核心代码与执行流程下面进入正题。我会给出一个最小但可运行的 Coding Agent 实现语言选 Python模型交互使用 OpenAI 兼容的 function calling API。你可以用任何支持 function calling 的模型服务只要把base_url和model替换成你自己的配置即可。我把核心代码拆成两部分工具注册与执行器、Agent 主循环。完整代码不长但每一行都是前面设计思路的直接落地。为了保持文章可读我略掉了一些文件读写错误的细粒度处理但保留了最关键的安全和异常分支。完整可跑版本建议你在本地仓库里逐步补齐。3.1 Agent 循环骨架用 function calling 驱动“决策—执行—观察”启动 Agent 前先定义工具列表。这里我用了一个build_tool_schemas()函数把每个工具的 JSON Schema 准备好这样模型才能理解工具的入参格式。每个 schema 都要写好描述模型会基于描述决定何时调用工具所以不要把 description 写得含含糊糊。import json import os import subprocess from pathlib import Path from openai import OpenAI WORKSPACE Path(os.getenv(CODING_AGENT_WORKSPACE, ./repo)) ALLOWED_PREFIXES ( pwd, ls, find, grep, cat, python, pytest, git status, git diff, git log, ) RUN_TIMEOUT 30 MAX_STEPS 20 client OpenAI( api_keyos.getenv(LLM_API_KEY, none), base_urlos.getenv(LLM_BASE_URL, http://127.0.0.1:8000/v1), ) def build_tool_schemas(): return [ { type: function, function: { name: view_file, description: 查看指定文件的指定行区间。start_line 和 end_line 省略时默认查看前 200 行。, parameters: { type: object, properties: { path: {type: string, description: 仓库内的相对路径}, start_line: {type: integer, description: 起始行号从 1 开始}, end_line: {type: integer, description: 结束行号}, }, required: [path], }, }, }, { type: function, function: { name: grep_search, description: 在仓库中搜索关键词返回匹配的文件和行内容。, parameters: { type: object, properties: { pattern: {type: string, description: 要搜索的关键词或正则}, path: {type: string, description: 搜索目录默认仓库根目录}, }, required: [pattern], }, }, }, { type: function, function: { name: edit_file, description: 编辑文件。用 old_string 定位文本替换为 new_string。old_string 必须唯一匹配。, parameters: { type: object, properties: { path: {type: string, description: 仓库内的相对路径}, old_string: {type: string, description: 被替换的旧文本}, new_string: {type: string, description: 替换后的新文本}, }, required: [path, old_string, new_string], }, }, }, { type: function, function: { name: run_command, description: 在仓库根目录执行一条终端命令。只允许白名单内的命令。, parameters: { type: object, properties: { command: {type: string, description: 要执行的命令}, timeout: {type: integer, description: 超时秒数默认 30}, }, required: [command], }, }, }, { type: function, function: { name: finish_task, description: 任务已完成提交最终总结并停止循环。, parameters: { type: object, properties: { summary: {type: string, description: 给用户看的任务完成总结}, }, required: [summary], }, }, }, ]这个 schema 有两个设计点要说明。一是很多教程喜欢让模型直接输出最终答案但 Coding Agent 场景里这样做不靠谱因为缺少“主动结束”的信号模型可能会无限追加操作所以我特意增加了finish_task工具。二是view_file的区间参数是可选的初版实现可以直接在函数里做默认行数截断避免模型在没看过文件长度时就传入一个超大的 end_line。接下来是工具执行器。它做的事情很简单根据函数名分发到真实实现捕获一切异常把结果统一转成字典返回。这里有个容易被忽略的点工具执行失败时不要把异常直接抛到 Agent 循环外而是把错误信息作为正常结果返回给模型。模型会读取错误信息自行修正参数后再次尝试。这是一个非常重要的 Agent 容错机制。def view_file(path, start_line1, end_line200): full_path WORKSPACE / path if not full_path.exists(): return {ok: False, error: f文件不存在: {path}} lines full_path.read_text(encodingutf-8).splitlines() total len(lines) start_line max(1, start_line) end_line min(total, end_line) selected lines[start_line - 1:end_line] return { ok: True, path: str(full_path), total_lines: total, content: \n.join(f{i start_line}: {line} for i, line in enumerate(selected)), } def grep_search(pattern, path.): full_path WORKSPACE / path if not full_path.exists(): return {ok: False, error: f目录不存在: {path}} result subprocess.run( [grep, -rn, --include*.py, pattern, str(full_path)], capture_outputTrue, textTrue, timeoutRUN_TIMEOUT, ) return { ok: result.returncode 0, matches: result.stdout[:4000], error: result.stderr[:2000] if result.returncode ! 0 else , } def edit_file(path, old_string, new_string): full_path WORKSPACE / path if not full_path.exists(): return {ok: False, error: f文件不存在: {path}} text full_path.read_text(encodingutf-8) if old_string not in text: return {ok: False, error: old_string 在文件中未找到请先读取文件确认原文} if text.count(old_string) 1: return {ok: False, error: old_string 在文件中出现多次请提供更长的上下文} full_path.write_text(text.replace(old_string, new_string, 1), encodingutf-8) return {ok: True, path: str(full_path), message: 编辑成功} def run_command(command, timeoutRUN_TIMEOUT): if not command.startswith(ALLOWED_PREFIXES): return {ok: False, error: f命令不在白名单中: {command}} try: result subprocess.run( command, shellTrue, capture_outputTrue, textTrue, timeouttimeout, cwdWORKSPACE, ) return { ok: result.returncode 0, stdout: result.stdout[-4000:], stderr: result.stderr[-2000:], } except subprocess.TimeoutExpired: return {ok: False, error: f命令执行超时超过 {timeout} 秒} def finish_task(summary): return {ok: True, summary: summary} TOOL_EXECUTORS { view_file: view_file, grep_search: grep_search, edit_file: edit_file, run_command: run_command, finish_task: finish_task, }关于上面的实现有几点实操经验值得展开。第一run_command一定要放在白名单机制后方而且白名单位置要在所有业务逻辑之前防止命令构造绕过我见过有人把白名单检查写在 subprocess 调用之后那就等于没有白名单。第二edit_file的 old_string 唯一性检查非常重要否则模型在修改多处相似代码时可能改错位置。第三所有返回结果都做了截断这是避免上下文被几百行编译日志塞爆的第一道防线。Agent 主循环的代码相对简短但要注意消息的组装方式。每次模型返回工具调用时要把完整的 assistant 消息追加到 messages然后为每个工具调用追加 tool 结果消息。tool 结果消息必须带tool_call_id才能和对应的工具调用请求关联上否则 API 会报错。def run_agent(user_task: str): messages [ { role: system, content: ( 你是一个运行在用户仓库里的编程助手。你只能通过工具和仓库交互。 每次行动前先观察当前状态再决定调用哪个工具。 修改文件前先用 view_file 或 grep_search 确认内容。 修改完成后用 run_command 运行相关测试。 任务全部完成后调用 finish_task 提交总结。 ), }, {role: user, content: user_task}, ] tool_schemas build_tool_schemas() for step in range(MAX_STEPS): print(f--- step {step 1} ---) response client.chat.completions.create( modelos.getenv(CODING_AGENT_MODEL, qwen2.5-coder:14b), messagesmessages, toolstool_schemas, tool_choiceauto, ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: print(No tool call, stop.) break for call in msg.tool_calls: try: args json.loads(call.function.arguments or {}) except json.JSONDecodeError: args {} executor TOOL_EXECUTORS.get(call.function.name) if not executor: tool_result {ok: False, error: f未知工具: {call.function.name}} else: try: tool_result executor(**args) except TypeError as e: tool_result {ok: False, error: f参数错误: {e}} except Exception as e: tool_result {ok: False, error: f工具执行异常: {e}} messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(tool_result, ensure_asciiFalse), }) return messages这段代码把“循环”呈现得很直白请求模型、判断有没有工具调用、执行工具、回传结果、再请求模型。里面有几个容易踩坑的地方。一是messages.append(msg)之后如果你再用msg.tool_calls去取调用列表需要把msg对象原样追加而不是把msg转成字典后追加否则后面的会话会缺字段。二是模型返回的工具参数偶尔不是合法 JSON所以json.loads一定要捕获异常并把错误提示回传给模型让它自己修。三是如果某个工具执行器抛了未捕获异常整个循环就断掉了所以我在执行器外层又包了一层通用异常捕捉确保循环不会因为一个小问题直接崩溃。3.2 让 Agent 真正改代码read / edit 工具的落地细节很多人在这一步会踩一个大坑让模型直接输出完整文件内容然后把整个文件覆盖写入。这种做法对大模型来说有很高的出错率容易在文件较大时“丢尾巴”或者“改错行”。我更推荐上文这种“old_string / new_string 局部替换”的方式它和人工改代码的习惯更接近也更容易做回滚。edit_file的定位逻辑其实就是一个字符串替换看起来简陋但配合上唯一性检查后实际效果相当稳定。因为模型在修改前会先用view_file查看原文拿到的是带行号的内容它只要把要改的那几行原样抄进 old_string 即可。对模型来说“抄一段原文再给出新文本”比“凭空生成整个文件”要容易得多。如果一段代码在文件里有多个相似片段模型第一次替换会失败返回“出现多次请提供更长的上下文”。此时模型会重新读取更长的区间把包含文件名的注释行或函数定义一起作为 old_string从而做到唯一匹配。这其实是用工具的报错信息来引导模型自我修正。我在实测中发现正确设计工具错误信息的效果比在 system prompt 里写“请注意上下文”好得多因为前者是即时、具体、场景化的反馈。3.3 一次完整的任务推演从“修一个 bug”到“提交前自查”为了让你直观感受整个执行流程我用一个假想的小 bug 走一遍。假设仓库里有个src/calculator.py计算函数写成了return a - b但需求应该是return a b。用户输入修复calculator.py里加法函数的错误。第一轮模型看到任务调用view_file参数为{path: src/calculator.py}工具返回带行号的文件内容。第二轮模型定位到错误行调用edit_file参数为{path: src/calculator.py, old_string: return a - b, new_string: return a b}工具返回编辑成功。第三轮模型调用run_command参数为{command: python -m pytest tests/}工具返回{ok: true, stdout: 1 passed in 0.21s}。第四轮模型判断任务完成调用finish_task参数为{summary: 已将 calculator.py 的加法函数由减法修正为加法测试全部通过。}。从外面看整个过程像变魔术从内部看每一步都是上面代码里那个 while 循环的产物。我在写完第一版后最深的体会是Coding Agent 的“智能”其实来自两个方向的叠加一个方向是模型本身的推理能力另一个方向是工具链设计得好不好。如果你的 Agent 表现很蠢先别急着换更强的模型花时间检查工具描述、返回截断、错误处理这些工程细节往往收益更大。4. 跑起来之后最常见的 6 个翻车现场和排查思路任何 Agent 都不会一次就顺畅。我在真实调试中遇到的翻车现场比想象中多得多。这里整理几个高频问题按照现象、原因、排查思路三个维度来讲基本覆盖了从零构建 Coding Agent 最常踩的坑。4.1 模型不调用工具或者每次都调用同一个工具这个问题很典型。打开日志发现模型压根不调用任何工具直接给出一个“应该怎么改”的文本回答而不是真正动手。原因通常有两个一是工具 schema 的 description 写得太模糊模型把工具调用当成可选项二是模型本身对 function calling 格式不敏感尤其一些小参数模型更容易“偷懒”。排查方法是先看日志里模型返回的 message 是不是真的带tool_calls字段。如果没有就在 system prompt 里强调“你必须使用工具完成修改不能直接输出答案”并且在示例里给一个“观察后调用工具”的 few-shot 样例。另一个让人抓狂的情况是模型反复调用view_file把整个仓库文件都读了个遍就是不动手改。这往往是edit_file的错误信息在误导它比如它尝试替换某段文本失败后不尝试修改 old_string而是无限扩大读取范围。解决办法是给view_file加一个最大读取行数比如默认 200 行同时把edit_file的报错写得更具体指导模型“出现多次时带上函数名或行号上下文”。4.2 工具参数格式错误循环陷入“报错—重试—再报错”function calling 虽然比裸 JSON 生成稳定但不代表不会出错。尤其在模型上下文很长、工具数量增加后偶尔会出现参数缺字段、多字段、字段类型不对的情况。我在循环里捕获了TypeError和JSONDecodeError把错误信息原样返回给模型让模型根据错误修正参数。大多数情况下模型会看一遍错误信息然后重新构造参数。但如果连续两次参数格式都错就说明当前模型理解不了工具 schema这时我会在system prompt里补充一个工具调用示例。这里要特别提醒不要在工具执行器内部做“宽松处理”。比如view_file的 start_line 传成了字符串我就见过有人为了省事在函数里int(start_line)强转。短期内能解决问题但长期会让模型越来越不遵守 schema。正确做法是保持工具入参严格校验让模型学会按格式传参。4.3 上下文膨胀跑着跑着把 token 打满了Coding Agent 比普通对话更容易触发上下文超限因为它会在循环里不断追加工具返回结果。每次view_file返回几千字几次下来就积累到上万 token。如果任务比较复杂几十轮后很容易撞上模型的上下文窗口上限。我的处理策略有三个第一所有工具返回严格截断stdout和stderr都限制在 4000 字以内第二在循环里累计工具结果字节数超过阈值后把早期的工具结果摘要化或者只保留最后 N 轮消息第三如果用的模型支持max_tokens设置把它调到合理值避免单次生成太长。第一版不需要做太复杂的上下文管理先把截断做好就能解决大部分问题。如果要支撑大型仓库再去研究 RAG 或基于代码库索引的上下文检索比如把文件树、符号列表、相关文件摘要先塞进上下文而不是无脑读取整个文件。4.4 模型“自作主张”改了不该改的文件这是 Coding Agent 最需要警惕的问题之一。模型在完成任务的过程中可能会顺手调整一个看起来“相关”但其实不应该动的常量或者把某个函数的缩进风格改了。原因在于模型对“最小改动原则”的遵守程度不稳定。解决方法是多管齐下一是 system prompt 里强调“除非任务要求否则不要修改无关代码”二是工具层面给edit_file加上“每次修改前先记录 diff”的逻辑修改后调用git diff --stat单独查看三是用户侧在 Agent 运行完后强制 review 一次 diff不合理的直接git checkout回滚。我建议在接入了 git 的仓库里跑 Agent这样每一轮编辑都可以用git diff精确看到改动范围。假如你的仓库还没有纳入版本控制第一步不是调试 Agent而是先git init并提交一版基线。没有 git 的 Coding Agent就像没有安全带的赛车。4.5 死循环一步错步步错直到步数耗尽死循环是 Agent 最常见的失控形态。模型第一次测试失败后尝试了一个修复但还是失败于是再用另一个方法又失败如此反复。如果没有步数上限它会一直烧 token。我设置了MAX_STEPS 20但光有上限不够还要让模型“知错能改”。我在工具返回错误信息时会附带“如果连续两次执行同一动作结果相同请换一个思路”这样的提示把它作为 tool 消息内容的一部分返回。这相当于给模型装了一个简单的“反思开关”在连续失败时及时切换策略。另外把每次命令的退出码放进返回结果也很重要。模型看到ok: false和stderr才能判断这次失败是代码 bug 还是环境问题。否则它会盲目地把环境缺失的报错当成自己要修的 bug白白浪费很多轮。4.6 命令白名单卡住了正常操作白名单太严格也有问题。初期我只允许pytest结果模型想用python -m unittest时就被拦下了。这种误伤会降低 Agent 的自适应能力。我的调整是白名单允许python -m pytest、python -m unittest、python manage.py test等常见测试命令前缀同时允许git系列只读命令。命令是否进入白名单判断标准不是“这个命令有没有用”而是“这条命令是否可能对仓库造成无法回滚的破坏”。像rm -rf、git reset --hard、git clean -fdx这种高危操作坚决不放行。这里再给一个实战技巧如果你真的需要 Agent 执行高危操作不要直接放行命令而是让 Agent 先调用类似request_approval的工具把要执行的命令交给用户审批。权限控制粒度越细Agent 能处理的任务边界越宽。5. 从玩具到生产力Coding Agent 上生产前要做好的几件事如果你的第一版 Coding Agent 已经能在小型仓库里跑通流程恭喜你你已经跨过了最关键的门槛。但“能跑通 demo”和“能放心交给它干活”之间还有一段不短的距离。我自己实验下来下面这几件事是上生产前必须补上的否则它只能停留在玩具层面。5.1 加一层“评审/审批”而不是完全放手我一开始也幻想 Agent 全自动完成所有操作结果几次翻车让我老老实实加了人工确认环节。推荐的做法是Agent 在每一步修改前先生成一个“计划”明确列出要改哪个文件、怎么改、为什么改然后暂停等待用户确认。确认通过后再进入edit_file。验证命令比如pytest这种风险较低的操作可以直接自动执行但像依赖安装、数据库迁移这类影响面大的命令必须走二次审批。这个设计看似拖慢了效率实际上恰恰是 Coding Agent 能长期用的关键。它的价值不是替你完全免盯而是把“从需求到 diff”的时间从几小时压缩到几分钟。你只需要花几十秒看一眼 diff 合不合理剩下的重复劳动交给 Agent。反过来如果全自动运行一次错误修改可能就毁掉半天的工作成果。5.2 用 Git 做操作审计与回滚生产环境里的 Coding Agent必须和 git 深度绑定。我强烈建议每个任务开始前基于当前 main 分支创建一个特性分支或工作副本Agent 的所有改动都在这个副本上进行。任务结束后你只需要对比分支差异确认无误后合并。如果过程中任何一步出错直接丢弃分支重来成本几乎为零。工具层面也要做审计。给edit_file和run_command各加一层日志记录本轮执行了哪些工具、传了什么参数、返回了什么结果。虽然看上去多写了几行代码但排查问题时价值极大。没有日志的 Agent 是一个真正的黑箱出了问题只能靠猜。5.3 评估集和可观测性怎么搭上生产前还应该搭一个微型评估集。我的做法是准备 8 到 10 个典型任务每个任务对应一个仓库初始状态和一个预期结果。比如“修复某个文件中的 off-by-one 错误”“把某个函数的日志改成 logging 模块”“新增一个带单元测试的工具函数”。每次改动 Agent 的提示词或工具定义后都把这批任务跑一遍看通过率有没有变化。这样你才能知道自己是在改好它还是在改坏它。可观测性则对应“过程日志”。除了记录工具调用还要把每一步的 token 消耗、调用耗时、当前消息数都打出来。很多 Agent 问题不是一次性爆发的而是随着 token 膨胀慢慢劣化。有了这些数据你才能判断“模型在第几轮开始变得不听话”是上下文里的信息太多还是某个工具返回了干扰信息。我在这套最小实现里刻意没有引入重型的 Agent 框架。因为我的目的是理解原理而不是被框架的抽象层绑架。但当你准备处理更复杂的多文件重构、跨仓库任务或多个 Agent 协作时可以考虑引入成熟的 Agent 框架它们能帮你解决任务编排、上下文管理、工具调度等通用问题。到那时候你已经知道底层发生了什么再用框架会顺手得多。最后分享一个小技巧如果你打算在真实项目里试用自己写的 Coding Agent我建议在 system prompt 里让它先输出一个PLAN.md也就是把任务拆成步骤写清楚再开始动手。这个习惯可以显著提高任务的完成质量。模型在写计划的过程中会重新理解需求也更容易发现自己对任务的误解从而在动手之前就纠正方向。我自己用下来的体会是这个步骤几乎不增加额外成本却能减少大概三分之一的无用改动。创建一个分支让它自己写计划执行计划最后你只看 diff 和总结这套流程已经足够应付绝大多数小型编程任务了。