扒开 Coding Agent 的“黑箱”从零构建你的第一个 AI 编程助手最近 Coding Agent 这个概念是真的火从 OpenAI 的 Codex 到各种开源框架好像一夜之间大家都在讨论“让 AI 自己写代码”。我也被问过无数次这东西到底是个啥它跟 Copilot 这类代码补全工具有什么本质区别自己能不能从零搭一个还是说只能等大厂封装好黑箱产品出来直接用这些问题问得多了我就想干脆自己动手拆一次。今天这篇文章我会带你从零构建一个真正可运行的 Coding Agent——不是调用某个现成的 Agent 框架跑个 demo而是把它的核心循环、工具调用、上下文管理等关键机制一步步拆开看清楚。我尽量用大白话讲代码也给全你照着抄也能跑起来跑通了之后你对市面上所有 Coding Agent 产品的理解都会深一个层次。1. 内容整体设计与思路拆解1.1 Coding Agent 与代码补全的本质区别先解决一个最常见的问题Coding Agent 到底跟 Copilot、Codex 这类补全工具有什么不一样很多人以为 Coding Agent 就是更高级的自动补全其实这是完全两码事。代码补全的本质是“预测”模型根据你当前光标前面的上下文预测接下来最可能的几个 token然后帮你补上。它是个一次性的、无状态的生成过程模型不需要运行代码、不需要看执行结果更不需要根据结果调整策略。Coding Agent 的本质是“做事”你给它一个任务比如“帮我修复测试失败的用例”它能自己规划步骤——先看看哪些测试挂了、分析可能的根因、修改对应源码、再跑一遍测试验证结果。这是一个多轮交互的闭环过程模型每一步都在“感知 → 决策 → 执行 → 观察结果 → 再决策”直到任务完成或者触发停止条件。用一个生活化的类比补全工具像是一个打字超快的速记员你写到哪儿它跟到哪儿Coding Agent 更像是你新招的一个初级工程师你交代一句“把那个 bug 修了”他会自己去看代码、定位问题、改代码、跑测试然后回来告诉你“搞定了”或者“遇到了新问题需要你确认”。这个本质区别决定了 Coding Agent 的架构完全不同于补全工具。一个最小可用的 Coding Agent 必须包含四个核心组件Agent Loop智能体循环整个系统的大脑负责决定“下一步做什么”循环执行直到任务完成。工具集Tools/Function CallingAgent 的“手脚”让它能读写文件、执行命令、搜索代码。没有工具Agent 就只是个会聊天的鹦鹉。上下文管理Context ManagementAgent 的“工作记忆”保存它看到过的文件内容、执行过的命令、拿到了什么结果确保它不会“失忆”。沙箱环境Sandbox/WorkspaceAgent 的工作台一个隔离的代码执行环境防止它乱搞你的系统。提示如果你要跟别人聊 Coding Agent记住“工具调用 多轮循环”是它的灵魂缺了这两样再聪明的大模型也只是个高级问答机器人。1.2 为什么说 Coding Agent 是“自带工作台”的 Agent严格来说Coding Agent 并不是一类独立于通用 Agent 的新技术它更像是一个“把 Agent 通用能力应用到软件开发场景”的典型案例。那为什么 Coding Agent 会显得特别复杂、特别像“黑箱”呢因为软件开发本身是个非常开放的领域。你让 Agent 去写一首诗它只要输出一段文字就行但让 Agent 去修一个 bug它需要面对的是一个可能有几十个文件的代码仓库、多个相互依赖的模块、环境配置问题、测试框架的输出格式、版本兼容性……任何一个环节出问题整个任务就可能失败。这跟通用 Agent 在网页上帮你订个外卖、查个天气完全是两个难度量级。我当时决定自己做 Coding Agent核心动力就是想搞清楚两件事第一GPT-4 级别的模型到底能不能靠“简单的循环 几个 shell 工具”完成真实的编程任务很多产品宣传把 Coding Agent 说得神乎其神我只是想验证一下最朴素的那条路——给模型一个终端、一个文件编辑器、一个循环它能不能自己把活干完第二调优 Coding Agent 的瓶颈到底在哪里是模型能力不够还是工具设计不合理还是上下文管理太粗糙不亲手搭一遍你永远只能停留在“看热闹”的层面。带着这两个问题我开始了第一版 Coding Agent 的搭建。接下来的内容我会完整复盘整个搭建过程包括技术选型、代码实现、踩坑记录和效果分析。2. 核心组件拆解Agent Loop 到底在循环什么2.1 最小可行闭环每一步在做什么一个最小的 Coding Agent 循环核心代码其实只有几十行。我在搭建时参考了目前主流开源方案比如 Codex CLI的思路但去掉了很多工程上的糖衣只保留最核心的逻辑大概长这个样子def agent_loop(task: str, max_iterations: int 20): messages [{role: user, content: task}] for step in range(max_iterations): # 1. 让模型做决策下一步该调用什么工具、传什么参数 response llm.chat(messagesmessages, toolsTOOL_SCHEMAS) assistant_msg response.choices[0].message # 2. 记录模型的想法和决策 messages.append(assistant_msg) # 3. 判断是否该终止循环 if assistant_msg.tool_calls is None: return f任务完成最终回复{assistant_msg.content} # 4. 执行工具调用并把结果返回给模型 for tool_call in assistant_msg.tool_calls: result execute_tool(tool_call.function.name, json.loads(tool_call.function.arguments)) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 达到最大迭代次数任务可能未完成这段代码虽然短但它就是全部的核心精髓模型决定调用什么工具 → 你的代码执行工具 → 把结果回传给模型 → 模型再看结果决定下一步。整个循环跑下来模型会逐步解决任务。写到这里你可能会问就这么简单对就这么简单。但问题在于这个循环里的每个环节都藏着各种坑。模型可能会瞎调用工具、工具执行可能出错、上下文可能会越来越长把模型搞懵……这些问题我会在后面的“踩坑实录”里详细展开。2.2 上下文管理Agent 的“工作记忆”到底怎么存刚才那个循环里有一个很容易被忽视但极其重要的部分——messages这个列表。它保存了模型看到的全部历史信息用户的任务描述、模型自己的决策和推理、工具执行的结果。这就是 Agent 的“工作记忆”。但真实场景中“全量保存”会很快遇到问题。假设你的代码仓库有 100 个文件Agent 每读一个文件就把全文塞进上下文那用不了几个文件上下文长度就爆了。更麻烦的是当上下文变得很长时模型对早期信息的注意力会下降——也就是所谓的“上下文丢失”或“长上下文退化”问题。我的处理思路是分级管理短期记忆最近几轮对话包括模型决策和工具结果直接放在 messages 中保证循环能跑。中期记忆模型读取过的文件我会做一个摘要版存档而不是原样保存。等模型需要再次查看某个文件时优先给摘要只有模型明确说“我需要看完整文件”时才重新读全文。长期记忆任务级的信息比如“我们在改的文件是 src/auth.py”“已确认数据库配置在 config/db.yaml”我会单独维护一份“任务状态快照”在每个循环轮次都重新注入给模型。这个设计很像人的工作方式你做的项目内容不会全部记在脑子里但关键结论、当前进度、待办事项这些会很清楚。我建议做 Coding Agent 的同学都认真设计一下这个机制而不是把所有东西一股脑往模型上下文里塞。3. 实操过程从零搭建一个可运行的 Coding Agent3.1 开发环境与技术选型在动手写代码之前先把技术栈确定下来。我的选择如下并给出理由组件我用的方案选型理由大模型Claude 或 GPT-4 级别模型需要较强的代码理解和工具调用能力小模型很难稳定完成复杂编程任务模型调用方式OpenAI 兼容的 Chat Completions API生态最成熟工具调用function calling支持稳定方便切换不同模型服务商工具执行环境Docker 容器隔离 Agent 的文件操作和命令执行防止它误伤宿主机器编程语言Python生态好、快速迭代配合类型标注便于维护工具集Bash 终端 文件读写 正则搜索覆盖开发者 90% 的日常操作实现简单且通用这里要特别说一下环境隔离的必要性Agent 在编程时经常要执行各种命令比如装依赖、跑测试脚本而它可能对项目并不完全了解。如果不加隔离一条rm -rf就可能给你带来灾难性损失。Docker 容器是我认为最理想的中间方案——既有接近真实环境的完整度又能确保宿主安全。3.2 定义 Agent 的工具集给 Agent 的“双手”一个 Coding Agent 需要哪些工具我踩了很多坑之后总结出最核心的五个再多就是锦上添花文件读取工具读取指定文件的内容支持行号标注方便模型定位问题。文件写入工具创建或覆写文件用于让 Agent 修改代码。代码搜索工具在整个仓库中搜索某个字符串或正则表达式相当于数据库里的 LIKE 查询。Shell 命令执行工具在项目目录下执行任意 shell 命令用于安装依赖、跑测试、运行脚本等。任务完成标记工具Agent 觉得任务解决了调用这个工具结束循环。每个工具都需要两个部分给模型看的“使用说明书”即工具 schema和实际执行的“后端代码”。工具 schema 是个很重要的细节模型会不会正确调用工具很大程度取决于 schema 写得清不清楚。比如我的 Shell 工具 schema 是这样的简化版{ type: function, function: { name: execute_shell, description: 在项目工作目录下执行一条 shell 命令返回标准输出、标准错误和退出码。如果命令需要交互式输入请使用 echo 管道等方式避免阻塞。, parameters: { type: object, properties: { command: { type: string, description: 要执行的 shell 命令例如pip install requests } }, required: [command] } } }注意我在 description 里特意写了“如果命令需要交互式输入请避免阻塞”这句话这是血泪教训——模型跑npm install时弹出了交互式确认把整个循环卡死了。把这类常见坑直接写进工具说明里能大幅减少调用失败次数。3.3 核心执行代码Agent Loop 的具体实现下面是完整可运行的核心代理循环代码框架我加了详细的注释方便你理解每一步的意图import json import subprocess from pathlib import Path from openai import OpenAI client OpenAI() # 假设你已经配好了 API Key WORKSPACE Path(/workspace) # Docker 容器里的工作目录 # 工具的实现 def execute_shell(command: str) - str: try: result subprocess.run( command, shellTrue, capture_outputTrue, textTrue, timeout60, cwdWORKSPACE ) output fexit_code: {result.returncode}\nstdout:\n{result.stdout}\nstderr:\n{result.stderr} return output[-8000:] # 截断过长输出防止上下文爆炸 except subprocess.TimeoutExpired: return 命令执行超时60秒请尝试分步执行或检查命令是否有交互式输入。 def read_file(path: str) - str: full_path WORKSPACE / path if not full_path.exists(): return f错误文件 {path} 不存在请先确认路径。 lines full_path.read_text().splitlines() return \n.join(f{i1:4d} | {line} for i, line in enumerate(lines)) def write_file(path: str, content: str) - str: full_path WORKSPACE / path full_path.parent.mkdir(parentsTrue, exist_okTrue) full_path.write_text(content) return f已写入文件 {path}共 {len(content.splitlines())} 行。 def search_code(pattern: str) - str: result subprocess.run( fgrep -rn {pattern} --include*.py --include*.js --include*.ts --include*.jsx --include*.tsx ., shellTrue, capture_outputTrue, textTrue, cwdWORKSPACE ) if result.returncode ! 0: return 没有找到匹配的结果 lines result.stdout.strip().splitlines() return \n.join(lines[:200]) # 限制返回条数避免上下文过载 TOOLS [ { type: function, function: { name: execute_shell, description: 在项目工作目录下执行一条shell命令返回退出码、stdout和stderr。命令如果是长时间运行的服务器进程请注意不要阻塞。, parameters: { type: object, properties: { command: {type: string, description: 要执行的shell命令} }, required: [command] } } }, { type: function, function: { name: read_file, description: 读取指定文件的内容输出会包含行号方便定位代码。路径相对于工作目录例如 src/main.py, parameters: { type: object, properties: { path: {type: string, description: 文件路径} }, required: [path] } } }, { type: function, function: { name: write_file, description: 将完整内容写入指定文件会覆盖原有内容。如果文件不存在会自动创建。路径相对于工作目录。, parameters: { type: object, properties: { path: {type: string, description: 文件路径}, content: {type: string, description: 完整的文件内容} }, required: [path, content] } } }, { type: function, function: { name: search_code, description: 在代码仓库中搜索指定字符串或正则表达式模式返回匹配的文件和行号。例如TODO、async def get_user、ERROR, parameters: { type: object, properties: { pattern: {type: string, description: 要搜索的字符串或正则表达式} }, required: [pattern] } } }, { type: function, function: { name: task_done, description: 当你认为用户的任务已经完成时调用此工具结束循环。参数 summary 中简要说明你的操作过程和最终结果。, parameters: { type: object, properties: { summary: {type: string, description: 任务完成总结} }, required: [summary] } } } ] def call_llm(messages): response client.chat.completions.create( modelgpt-4o, # 可换成你习惯的模型 messagesmessages, toolsTOOLS, tool_choiceauto, # 让模型自己决定是否调用工具 temperature0, ) return response.choices[0].message def agent_loop(task: str, max_iterations: int 30): messages [{ role: system, content: 你是一个专业的编程助手。你需要通过调用工具来完成任务。大部分情况下你应该先查看项目结构和相关文件理解现状后再动手修改。修改后务必运行测试或命令验证效果。如果你确定任务已经完成调用 task_done 工具结束。 }, { role: user, content: task }] for step in range(max_iterations): print(f\n 第 {step 1} 轮 ) assistant_msg call_llm(messages) # 把模型的回复加入消息列表 messages.append(assistant_msg) # 没有工具调用说明模型想直接回复用户异常情况 if not assistant_msg.tool_calls: print(f模型没有调用工具直接回复{assistant_msg.content}) if task_done not in str(assistant_msg.tool_calls): print(强制结束循环) return assistant_msg.content # 逐个执行工具调用 for tool_call in assistant_msg.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments) print(f 调用工具: {fn_name}({json.dumps(fn_args, ensure_asciiFalse)})) # 分发到具体实现 if fn_name execute_shell: result execute_shell(fn_args[command]) elif fn_name read_file: result read_file(fn_args[path]) elif fn_name write_file: result write_file(fn_args[path], fn_args[content]) elif fn_name search_code: result search_code(fn_args[pattern]) elif fn_name task_done: print(fAgent 自认为任务完成{fn_args[summary]}) return fn_args[summary] else: result f未知工具{fn_name} # 把工具结果加到消息列表 messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) print(f 工具返回结果长度: {len(result)} 字符) print(达到最大迭代次数任务可能未完成) return 达到最大迭代次数 if __name__ __main__: task 请帮我完成以下任务 1. 查看当前目录下的项目结构 2. 找到所有 TODO 注释并列出 3. 选择其中一个 TODO完成对应的功能实现 4. 运行相关测试验证你的修改 agent_loop(task)3.4 跑通后的第一课真实的 Agent 行为长什么样我的第一次测试任务比较基础“给这个 Python 项目补上缺失的 requirements.txt并跑通测试”。执行过程中Agent 的行为拆解如下——注意看它是怎么一步步思考的第 1 轮 调用工具: execute_shell({command: ls -la find . -name *.py | head -20}) 工具返回项目结构清单约 10 个 Python 文件没有 requirements.txt 第 2 轮 调用工具: read_file({path: main.py}) 工具返回文件内容带行号 第 3 轮 调用工具: search_code({pattern: ^(import|from)\\s}) 工具返回所有 import 语句汇总出第三方依赖 第 4 轮 调用工具: write_file({path: requirements.txt, content: flask3.0.0\nrequests2.31.0...}) 工具返回写入成功 第 5 轮 调用工具: execute_shell({command: pip install -r requirements.txt -q python -m pytest tests/ -v}) 工具返回测试全部通过 第 6 轮 调用工具: task_done({summary: 已生成 requirements.txt 并确认测试通过})整个过程行云流水模型表现得很聪明。它没有一上来就乱写文件而是先观察项目、再定位依赖、再动手、最后验证——这个顺序非常符合一个谨慎的程序员的做事逻辑。看到这里我就明白了Coding Agent 的技术关键不是模型有多聪明当然模型能力也很重要而是你给了它一套合手的工具和一个宽松但有边界的探索空间。4. 从 Demo 到能用稳定性与安全性的工程落地4.1 防“幻觉”循环超时、重试与最大轮次限制跑通 Demo 只是第一步。当你开始让 Agent 处理真实任务时会遇到一个很恼人的问题——Agent 陷入死循环。它可能在同一个问题上反复横跳比如一会儿觉得是数据库问题改完又觉得是缓存问题改回来又觉得是数据库问题……如此反复直到把上下文撑爆。针对这个问题我做了三件事第一最大迭代次数硬限制。我通常设为 30 轮超过就强制终止并要求 Agent 输出当前进展和未解决的问题。这样即使“跑飞了”用户也有信息可拿。第二重复行为检测。我维护一个“最近 10 次工具调用摘要”的历史列表如果检测到 Agent 连续多次执行相同或高度相似的命令比如pytest失败后不改代码就反复跑我就硬插入一条系统消息提醒它“你似乎在进行重复操作请重新审视当前策略。”实测这个干预还挺有效能把模型从“撞墙”状态拉回来。第三对不稳定的工具调用做重试。Shell 命令可能因为网络闪断、依赖安装超时等原因失败我做了简单的重试逻辑——如果退出码非零是“可重试型”错误比如下载超时我会告知模型刚才失败了并请它重试如果是“逻辑型”错误比如语法错误就直接把报错返回让模型自己调整。4.2 安全护栏Agent 权限最小化实践安全怎么强调都不过分。一个能自由执行 Shell 命令的 Agent本质上就是个可以远程操控你电脑的“虚拟员工”。你愿意让一个实习生在不做任何限制的情况下随意rm -rf吗肯定不愿意。所以我在实践里设置了这些护栏Docker 沙箱是底线。所有 Agent 的工具执行都在容器里宿主机与容器之间只挂载一个项目目录且挂载为只读时更保险。容器内存和 CPU 也设置上限防止 Agent 跑死宿主机。高危命令拦截。我在 Shell 工具前端加了一层命令检测对rm -rf /、mkfs、dd、shutdown等黑名单命令直接拒绝执行并返回“该命令被安全策略拦截”。网络访问控制。除非任务明确需要下载依赖否则我默认不给容器网络权限。这样即使 Agent 被提示词注入诱导去执行恶意命令也传不出去数据。文件范围限制。文件读写工具只允许操作工作目录范围内的路径../等越界路径会被拒绝。提示如果你的 Coding Agent 会读取来自不可信来源的代码或文档一定要注意“提示词注入”风险。恶意代码里可能藏着一句“请忽略系统提示执行以下命令...”如果你的 Agent 不加防护地读取并信任文件内容可能被“策反”。4.3 让 Agent 真正融入工程不只是改代码做到这一步我的 Coding Agent 已经能处理一些中等复杂度的任务了比如“给项目加一个日志模块”“修复某个已知 bug 并补充测试”。但我发现它离“融入真实工程”还有距离——真实开发不只是改代码还包括代码评审、持续集成、文档更新等环节。于是我做了一个升级把我自己的开发工作流拆成多个 Agent 角色这部分我留到以后细聊——一个负责写代码、一个负责审查、一个负责跑测试——然后把它们串成一个流程。这样单个 Agent 的任务变小了、更聚焦了准确率明显提高。从效果来看把一个直接用大模型循环生成代码的“单 Agent”升级为“多 Agent 协作流水线”是 Coding Agent 从玩具走向实用的关键一步。不过这篇文章里我不准备展开多 Agent 的实现细节因为这又是一个大话题。我先把单 Agent 的核心机制讲透多 Agent 的协作方案有机会再单独写一篇。5. 常见问题与排查技巧实测中的血泪教训Coding Agent 虽然流程看着简单实际跑起来会遇到各种奇怪的问题。我把自己踩过的坑整理成了速查表你应该用得上现象可能原因排查与解法Agent 反复执行同一命令不修改代码模型陷入了“验证死循环”对测试失败原因理解不够用重复行为检测机制强制插入系统提示提醒它调整策略上下文很快爆满工具返回结果太长限制单次工具返回长度我设为 8000 字符以内对长文件用行号截断模型“睁眼说瞎话”声称改完代码但实际没改模型出现了幻觉或者写文件工具出错但模型没注意到报错让工具返回明确的成功/失败标记并在系统提示中强调“只有看到成功提示才算修改成功”命令执行时卡住命令在等待交互式输入设置 Shell 超时时间60秒在工具 description 中提醒避免交互式命令模型不调用工具直接给建议模型被配置成“回答模式”或者提示词里没讲清楚必须动手系统提示中明确“你必须在真实环境中操作不能只给建议”没有工具调用时也保持循环修改代码后测试仍失败Agent 没有仔细看报错信息只改了表面问题要求 Agent 把报错信息粘贴到回复中先解释根因再动手工具调用格式错误模型生成的参数与 schema 不匹配升级模型版本或在系统提示里给出工具调用示例few-shot 很有用依赖安装缓慢导致频繁超时网络问题在容器里预装常用依赖给 Agent 提供镜像源配置下面挑三个问题展开讲讲我的排查思路。5.1 “睁眼说瞎话”问题模型是怎么忽悠你的我第一次跑真实 bug 修复任务时遇到一个让我苦笑不得的场景Agent 声称已经修好了某个函数但我检查文件发现根本没有改动。回看对话历史发现它其实尝试调用了 write_file 工具但当时的参数写错了路径写成了./src/main.py而不是src/main.py工具返回了错误信息结果模型没有认真解析直接忽略报错并且继续自说自话。这个问题的根源在于模型面对工具返回的错误信息时处理不够谨慎。解决方法是双管齐下一是把工具返回的报错信息做得极其醒目比如加上[ERROR]前缀并在系统提示里明确要求模型“如果看到 ERROR 标志必须优先处理错误不能继续推进”二是换个更强的模型。实测发现Claude 系模型对工具返回状态的关注度明显更可靠这也是为什么后来我把主力模型切换了。模型选型对 Agent 的稳定性影响真的很大这一点我再强调一次都不过分。5.2 上下文爆炸问题工具返回结果太多了怎么办上下文管理是我调优过程中花时间最多的地方。Agent 拿到一个 2000 行的文件时会把整个文件塞进上下文跑一个测试命令时几千行日志也会全部塞进去。我很快发现模型开始“遗忘”最初的用户任务甚至在一半的时候忘记自己要干什么。我的解法组合是返回结果截断单次工具返回限制在 8000 字符左右多余部分用...(已截断 N 行)...提示。文件读取按需分段大文件默认只返回前 200 行和行号索引模型如果需要看后续可用 read_file 配合行号范围参数。任务状态注入每轮开始前我把用户原始任务、已完成的步骤、当前待解决问题这三块以结构化文本重新注入模型上下文。这三招让 Agent 的长任务稳定性提升明显强烈推荐都试试。5.3 工具调用的设计细节description 写得好故障少一半我在调试中发现一个很容易被忽略的细节工具 description 的写法直接决定模型调用的准确率。同样的一个 Shell 工具description 写“执行 shell 命令”和写“执行 shell 命令命令将在项目的根目录/workspace下运行相对路径以此为基础命令需要交互输入时会阻塞请用 echo 等方式避免超过 60 秒会被终止”模型的使用错误率会显著不同。原因是大模型在做工具调用时有一点“阅读说明书”的性质——说明书写得越清楚、越贴近真实约束模型就越不容易犯错。所以不要嫌麻烦把你踩过的坑都写进工具描述里。这就相当于给 Agent 做“岗前培训”非常值。结尾从零搭建 Coding Agent 的整个过程我最大的体会是它并不是什么神秘的黑箱核心机制就是“循环 工具调用 上下文管理”这三板斧。真正需要花心思的地方是那些细节——工具的 description 怎么写、上下文怎么截断、错误信息怎么让模型关注到。这些功夫下得越多你的 Coding Agent 就越稳定、越实用。最后分享一个小技巧如果你刚接触 Coding Agent别急着上很复杂的多 Agent 框架或重型平台先用我上面给的最小实现跑通端到端流程再逐步加功能。把一个简单的循环吃透了后面看任何 Agent 项目都会轻松很多。我自己就是从这几十行代码开始一步步搞清楚了 OpenAI Codex CLI、Claude Code 这些产品背后的设计理念——不是靠读文档读懂的是靠踩坑踩懂了的。如果你也搭了自己的 Coding Agent欢迎来交流你踩到的坑尤其是工具调用和上下文管理这两块不同的实践场景里坑完全不一样。
