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_key=os.getenv("LLM_API_KEY", "none"), base_url=os.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_line=1, end_line=200): full_path = WORKSPACE / path if not full_path.exists(): return {"ok": False, "error": f"文件不存在: {path}"} lines = full_path.read_text(encoding="utf-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_output=True, text=True, timeout=RUN_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(encoding="utf-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), encoding="utf-8") return {"ok": True, "path": str(full_path), "message": "编辑成功"} def run_command(command, timeout=RUN_TIMEOUT): if not command.startswith(ALLOWED_PREFIXES): return {"ok": False, "error": f"命令不在白名单中: {command}"} try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=timeout, cwd=WORKSPACE, ) 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( model=os.getenv("CODING_AGENT_MODEL", "qwen2.5-coder:14b"), messages=messages, tools=tool_schemas, tool_choice="auto", ) 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_ascii=False), }) 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 和总结,这套流程已经足够应付绝大多数小型编程任务了。