资讯动态

从零手写最小Coding Agent:图解Agent Loop与生产级工程骨架

发布时间:2026/10/26 21:49:32 来源:尧图企业网站定制
Coding Agent这个词最近几个月都快被说烂了。从OpenAI的Codex上线开始那句“welcome to codex”几乎成了AI编程圈子的接头暗号——大家突然意识到让AI自己读代码、改文件、跑命令、看报错再修正已经不是Demo阶段的东西而是真能塞进日常开发工作流里的生产力工具。但热闹归热闹真去打开一个Coding Agent的开源项目比如我们今天用来当解剖样本的Pi很多人大概率会有点懵又是工具注册、又是Agent Loop、又是流式输出还有一堆配置项根本不知道从哪下手。这篇文章想换个思路来聊。先带大家从0手写一个最小的Coding Agent代码量控制在200行以内能跑通“读代码—改代码—执行命令—看结果”这整条闭环然后再拿这个小玩具当参照系去拆解Pi这种生产级Agent的骨架看看它到底比我们的玩具多了哪几根骨头以及为什么要多。适合看的人有两类一是想搞清楚Agent底层原理、准备自己动手搞AI编程工具的开发者二是已经在用各种Coding Agent但想知道背后工程逻辑的同学。我先把话说清楚这文章不涉及模型调优也不涉及分布式训练只讲工程结构——只要你会Python、能调通任意一个大模型的API就能完全看懂。1. 动手之前先拆穿Coding Agent的三个底层概念1.1 Coding Agent不是ChatGPT套壳核心是“Agent Loop”很多同学第一次接触Coding Agent脑子里第一反应是“这不就是拿ChatGPT生成代码然后复制粘贴吗”。如果只是这样它根本不配叫Agent。真正让Coding Agent有“智能感”的是一个循环Agent把任务拆成步骤每一步调用一个或多个工具读文件、写文件、执行命令然后把工具返回的结果交给模型模型再决定下一步做什么。这个循环的专业叫法是Agent Loop本质上就是“思考”和“行动”交替进行。打个比方。普通AI编程助手像一个只会出主意的顾问坐在你旁边说“你应该改这个文件”“你应该跑一下那个测试”但动手的还是你自己。而Coding Agent是一个真正能上手干活的实习生他不仅能给出方案还能自己打开编辑器改代码、自己开终端跑测试、看到报错自己修你只在关键节点检查一下结果。“读工具结果→模型决策→再调工具→再看结果”这条链路就是Agent的闭环。只要闭环在哪怕模型本身并不顶尖也能靠不断试错把任务磨完闭环一旦断了模型再强也只是纸上谈兵。1.2 最小闭环里必须有哪几块积木一个最小的Coding Agent拆到不能再拆至少需要四块积木。第一是模型接口负责把系统提示、工具结果、对话历史打包成上下文让模型输出“下一步动作”。第二是工具定义给模型提供一组可调用函数的描述包括函数名称、参数结构和用途说明——模型本身不执行代码它只负责决定调哪个工具、传什么参数。第三是工具执行器真正去执行模型指定的函数并把执行结果返回给模型所有文件读写、命令执行都发生在这层。第四是循环控制判断对话是否继续、是否要发起新一轮工具调用以及什么时候该终止。这里有个新手极其容易搞混的认知点模型不是一个能随便执行代码的运行时它只是一个“决策器”输出的是结构化指令。比如说模型输出“我要调用read_file这个工具参数是src/main.py”你的程序必须解析出这个意图然后自己把文件读出来再把内容塞回给模型。这套“决策与执行分离”的设计是Agent能稳定跑起来的根基理解了它后面所有代码都好懂了。1.3 为什么拿Pi当解剖对象市面上Coding Agent相关项目并不少但我选Pi当解剖样本是因为它的代码结构足够“标准”。Pi不是一个玩具项目它拥有完整的生产级特征有配置系统、有工具注册中心、有会话状态管理、有流式输出甚至还有并发处理和重试机制。对一个想理解Agent骨架的人来说Pi就像一本写得比较工整的教科书——它的设计思路和你在生产环境里自己搭Agent时会遇到的问题几乎一一对应。后面我会把Pi的工程结构和我们写的那个最小实现逐条对比让你清楚地看到从一个200行的玩具到一个能上生产的框架中间到底加了些什么、为什么加。2. 从0实现最小Coding Agent一步步跑通第一个闭环2.1 技术选型Python OpenAI兼容协议让模型可替换考虑到代码要尽量短、你还要能直接跑起来我选了Python。模型调用方面现在几乎所有大模型厂商都提供了OpenAI兼容接口所以我们就直接用一个标准的ChatCompletion调用base_url换一下就能接不同模型。顺带提一下Pi的配置项里也有base_url这个参数它的作用就是配置模型服务地址——你可以用vLLM或者Ollama在本地起一个模型推理服务把base_url指到本地Pi就能跑在完全离线的环境里。这其实是生产级Agent的一个重要设计思想模型供应商是可替换的Agent的代码不应该绑定在某一家厂商的SDK上而是通过标准协议去对接。工具方面先实现三个基本的read_file读文件、write_file写文件、run_command执行shell命令。这三个工具已经足够完成“读代码—改代码—跑测试—看输出”的完整闭环。后面如果你想扩展比如加一个web_search工具也完全不影响架构只是往工具清单里多塞一个条目而已。2.2 工具定义与执行器给模型一份“能力说明书”工具定义的本质是给模型一张菜单。在OpenAI兼容协议里工具格式是JSON Schema包含名称name、描述description、参数结构parameters。描述信息非常关键因为模型就是靠它来判断什么时候该用这个工具的。描述写得太模糊模型容易在不需要的时候乱调用写得太具体又可能把模型的发挥空间限制死。我见过不少人在description里写“读取文件内容返回文件内容”这其实是句废话好的描述应该说明“什么场景下用这个工具、边界是什么”。比如读文件工具的描述我会写成“读取文件内容适合查看已有代码、配置或日志”这样模型看到“这个函数是干嘛的、里面有什么”这类需求时自然会优先想到它。执行命令工具的描述则会强调“适合运行脚本、测试、查看命令输出”。这些细节在文档里不显眼但实际跑起来差别很大——工具描述写得差模型会频繁选错工具表现出来的就是Agent行为很蠢。工具实际执行的函数也很直接。read_file就打开文件读内容write_file就写文件run_command就用subprocess跑命令并捕获stdout和stderr。我特意在返回结果里带上退出码因为模型需要知道命令是成功还是失败这直接决定它下一步是继续还是修正。2.3 搭Agent Loop决定什么时候该收手Agent Loop的核心逻辑非常朴素。第一步把系统提示、历史消息和工具描述拼成请求发给模型第二步拿模型的回复第三步检查回复里有没有tool_calls字段第四步有工具调用就执行工具、把结果追加到消息列表然后回到第一步继续循环第五步如果模型没有输出tool_calls说明它认为任务已经完成直接退出循环。但这里有个细节值得展开到底什么时候算“任务完成”。模型可能前面好几轮都在调工具突然某一轮不再输出tool_calls而是输出一段总结这个信号就是循环终止条件。还有一种情况是模型陷入死循环比如反复读同一个文件却不做任何修改这时候必须设置最大迭代次数作为安全阀我习惯把它配成10轮任务复杂可以调大但一定要有上限。另一个我踩过的坑是工具结果太长——一个文件几千行全塞回上下文直接就把模型的上下文窗口撑爆了。生产级方案是截断我后来的习惯是把返回内容截到2000个字符多余部分省略必要时提示“内容过长已截断”。2.4 完整代码能跑的200行最小Coding Agent下面是一版完整的Python实现注释写得比较详细。为了让你看清楚Agent Loop这副骨架我刻意没加任何花哨功能#!/usr/bin/env python3 # 最小Coding Agent约150行含注释 import json import subprocess import sys from openai import OpenAI # 换成你实际用的模型服务地址本地vLLM/Ollama或其他兼容OpenAI协议的服务都行 client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) SYSTEM_PROMPT 你是一个Coding Agent。你可以使用以下工具 - read_file: 读取文件内容 - write_file: 写入/创建文件 - run_command: 执行shell命令 规则 1. 每次只调用一个工具根据工具结果决定下一步。 2. 当任务真正完成时直接输出最终答案不要再调用工具。 TOOLS [ { type: function, function: { name: read_file, description: 读取文件内容适合查看已有代码、配置文件或日志。, parameters: { type: object, properties: {path: {type: string}}, required: [path], }, }, }, { type: function, function: { name: write_file, description: 写入或创建文件适合生成代码、修改文本文件。, parameters: { type: object, properties: { path: {type: string}, content: {type: string}, }, required: [path, content], }, }, }, { type: function, function: { name: run_command, description: 在shell中执行命令适合运行脚本、测试、查看输出。, parameters: { type: object, properties: {command: {type: string}}, required: [command], }, }, }, ] def read_file(path: str) - str: with open(path, r, encodingutf-8) as f: return f.read()[:2000] def write_file(path: str, content: str) - str: with open(path, w, encodingutf-8) as f: f.write(content) return f已写入 {path} def run_command(command: str) - str: result subprocess.run(command, shellTrue, capture_outputTrue, textTrue, timeout10) return fexit_code{result.returncode}\n{result.stdout[-2000:]}{result.stderr[-2000:]} TOOL_MAP { read_file: read_file, write_file: write_file, run_command: run_command, } def run_agent(task: str, max_steps: int 10) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task}, ] for _ in range(max_steps): resp client.chat.completions.create( modelgpt-4o-mini, # 换成你服务的模型名 messagesmessages, toolsTOOLS, tool_choiceauto, ) msg resp.choices[0].message messages.append({ role: assistant, content: msg.content, tool_calls: msg.tool_calls, }) if not msg.tool_calls: return msg.content or 任务完成 for tc in msg.tool_calls: func TOOL_MAP[tc.function.name] args json.loads(tc.function.arguments) result func(**args) messages.append({ role: tool, tool_call_id: tc.id, content: result, }) return 已超过最大步骤数任务未完成 if __name__ __main__: task sys.argv[1] if len(sys.argv) 1 else 用Python写一个斐波那契函数保存到fib.py然后运行并打印结果 print(run_agent(task))运行方式很简单pip install openai python agent.py 用Python写一个斐波那契函数保存到fib.py然后运行并打印结果跑起来之后你会看到它执行的大致流程模型先决定调用write_file创建fib.py然后用run_command执行文件最后输出结果。整个过程大概在30秒到1分钟之间具体取决于模型响应速度。如果用的是本地模型可能还会慢一点但逻辑是一样的。2.5 代码里藏着的三个关键细节第一个细节是系统提示词不是摆设。在这版实现里SYSTEM_PROMPT承担了“告诉模型你有哪些工具、该怎么用”的职责。我最初调试时吃过亏发现如果把提示词写成“你是一个AI助手”模型就真的只答话不调工具——它根本不知道自己有工具可用。后来把规则明确写在提示词里尤其是“根据工具结果决定下一步”“完成后直接输出最终答案”整个行为立刻正常了。第二个细节是工具执行的错误处理。我在handle_tool_call里用了try/except把异常信息作为结果返回给模型——虽然上面代码示例为了简洁没写出异常捕获但实战中这一步不能省。这个设计极其重要因为模型“看到”报错信息后才会自己修正。碰到文件不存在或命令退出码非0最好不要中断循环而是当作正常的工具结果喂回给模型让它自己决定怎么改。只有JSON解析错误或配置错误这种系统级问题才应该直接终止。第三个细节是消息历史的结构。每轮循环都要把模型回复追加进messages然后工具执行结果以roletool追加一条并且必须带上对应的tool_call_id。OpenAI的Python SDK对消息顺序和role有严格校验一旦role顺序不对请求直接400。这也是我见过最多人卡住的地方——不是模型不行是消息结构不对。3. 读懂Pi的生产级骨架对照最小实现拆解五大模块3.1 Pi整体架构模块多但有一条清晰主线Pi的代码比我们的小实现复杂得多但如果你顺着数据流去看会发现它其实就是把我们那几块积木——工具、模型、循环——放大成了一整套系统。打开Pi的源码一眼扫过去目录被拆成好几个packagecore、tools、agent、config、session、ui。虽然模块多但它有一条主线用户的任务从入口进来流向config模块完成初始化进入agent引擎开始循环循环里通过tools分发器调用具体工具session管理器在背后记录上下文ui层把每一步进展实时渲染出来。Pi在架构上有一个让我印象很深的点它把“模型调用”和“工具分派”之间做了一层抽象这层抽象类似消息总线和事件流。简单说Pi不是直接“调用函数再返回值”而是把工具调用动作包装成事件经过统一的分发器去执行。这样做的直接好处是可以在工具执行前后插入日志、权限检查、重试逻辑而且不会污染核心循环代码。我们的小实现如果想要升级第一个值得做的重构就是往这个方向走。3.2 第一根骨头配置系统让Agent换环境不用改代码咱们的最小实现里配置就是代码顶部的几个常量模型名、base_url、max_steps。够用归够用但生产环境不会有这么简单的事。Pi的配置系统支持命令行参数、环境变量、配置文件三种注入途径且带有优先级——命令行大于环境变量环境变量大于配置文件。这种设计对部署非常友好本地用默认配置跑到服务器上可以用环境变量覆盖模型地址在CI/CD里再用参数覆盖具体任务类型全程不用动一行代码。配置项本身也做了分层。模型相关一组、工具相关一组、行为相关一组。比如base_url就属于“模型提供者配置”这一层它允许你在同一个Agent里配置多个模型来源甚至做模型路由。还有一个安全细节API Key绝对不应该硬编码在代码里生产级Agent的配置系统必须支持从环境变量读取密钥并且确保密钥不出现在日志中。这一点看着不起眼但它决定了框架能不能多人协作、多环境部署。一个没有配置系统的Agent基本只能活在作者自己的电脑上。3.3 第二根骨头工具注册与管理从if-else到可插拔我们的小实现里工具就是一个字典加一个分支判断每新增一个工具就要改代码、改TOOLS列表、改TOOL_MAP。Pi把这一层做成了注册中心每个工具是一个独立的类或函数对象通过装饰器或者配置文件注册进框架。框架维护一份工具清单包括工具名称、描述、参数Schema、权限级别、运行环境等信息。注册中心的第一个好处是可插拔。你想给Pi加一个数据库查询工具不用碰核心代码只要新写一个工具类再注册进去。这符合工程上的“开闭原则”——对扩展开放对修改关闭。第二个好处是统一管控。所有工具都要经过注册中心才能被模型调用所以框架可以在工具执行前做一系列检查文件操作是否限定在工作目录内、命令是否命中危险指令黑名单、网络请求是否走代理等。生产环境对这类权限管理的要求很高因为我们没办法保证模型永远不“调皮”。Pi还有个小细节工具描述会占用上下文所以它要求工具作者必须把description写得精炼。工具一多这事的收益特别明显——描述太长的工具会挤占上下文窗口让模型注意力被稀释。如果你以后自己搭Agent工具描述能短则短。3.4 第三根骨头状态管理与会话上下文断点续传是底线我们的最小实现里状态就是messages这个列表会话结束就丢。Pi不一样它有完整的会话生命周期管理会话可以暂停、恢复、存档还能把历史会话当作新任务的起点。这在长任务场景下价值巨大。举个例子本地调试项目时Agent跑着跑着模型服务突然挂了。最小实现里上下文全丢下次只能从头开始而Pi里会话状态被持久化重启之后可以从上次进度继续跑。这就是生产级的“断点续传”。会话管理还会牵扯到上下文压缩。Agent跑久了历史消息越来越长模型上下文很快就会顶到天花板。Pi提供了摘要和裁剪机制历史消息超过阈值时把前面的对话总结成一小段摘要放到最前面然后清理原始消息。这个策略在长对话里几乎必不可少——不压缩的话任何Agent都撑不过30轮工具调用。另外Pi把session状态抽象成了独立数据结构而不是裸的messages数组这也让它能同时跑多个会话而不互相干扰。对生产级服务化部署来说并发会话隔离是底线。3.5 第四根骨头流式输出与实时交互反馈延迟决定体验最小实现是憋到循环结束才一次性返回结果命令行里还能忍要做成交互式工具就完全不行。Pi默认使用流式输出模型每生成一个token就推送到终端工具执行的每一步也实时打印。用户能“看着”Agent工作而不是干等一把梭。流式输出的实现比非流式复杂因为它牵涉到事件实时推送和界面刷新。Pi的做法是把事件流分成两类一类是模型的token流给用户“打字机”体验另一类是Agent自身的事件流比如“正在读取文件”“正在执行命令”“发现测试失败”。两者合在一起就是现在主流Coding Agent产品都有的交互效果。别小看流式输出的意义。对任何工具类产品反馈延迟直接决定用户的心理感受。一个3秒内就开始输出的工具和一个15秒空白后一次性吐出一大段结果的工具用户会坚定地认为前者“快很多”。这不是玄学是实实在在的体验差距。3.6 第五根骨头错误恢复与重试策略把不确定性变成可控事件最小实现遇到模型API超时会直接抛异常整个程序就没了。生产级Agent必须考虑错误恢复Pi在多个层面做了容错。首先是对临时故障的重试模型接口偶尔返回限流429或5xx错误这些通常过几秒就恢复Pi会按指数退避策略自动重试而不是立刻报错。其次区分“可恢复错误”和“不可恢复错误”可恢复的如测试用例失败、文件格式问题会作为上下文继续循环不可恢复的如目标文件不存在、模型输出格式非法会终止或切换策略。第三个层是模型输出解析容错。模型有时候不回合法的JSON甚至夹带额外文本。Pi的解析器会尝试提取JSON块、修正缩进错误。我们的最小实现里用的是json.loads一步到位格式一坏就完蛋这在生产环境远远不够。写到这里你会发现生产级Agent的骨架本质上就是一套把各种不确定性变成“可处理事件”的工程机制模型行为不确定、工具执行不确定、网络服务不确定框架的责任就是把这些不确定性一个个装进可控的轨道。4. 从玩具到生产级中间还差这四步升级4.1 并发与会话隔离生产级的Agent服务很少只服务一个用户。Pi支持同一时刻多个会话并发执行每个会话有自己的上下文、状态和工具环境。这对我们小实现的启示是不要把会话数据挂在全局变量上否则两个请求一交错数据就串了。实际开发里我习惯把会话对象封装成一个类每个请求对应一个独立实例。并发还会带来资源控制问题。多个Agent同时执行run_command会争抢服务器CPU和内存。生产环境一般会在工具执行层加一个信号量限制最大并发指令数甚至给每个工具单独配置并发上限比如最多同时跑3个shell命令。4.2 可观测性与日志我一直觉得可观测性是Agent框架最容易被忽略却又最重要的模块。最小实现里顶多print几行结果生产环境里这是灾难。Pi会为每次交互生成结构化日志记录模型请求参数、token用量、工具调用耗时、错误堆栈方便事后排查。如果你想让小实现走向生产级建议至少加一个中间件层把每轮循环的请求和响应都落盘。之后再跑Agent一旦发现结果不对翻日志就能定位到是哪一步决策出了问题。这个习惯能帮你省掉大量“玄学排障”时间。4.3 安全与权限边界Coding Agent本质上是一个“能替你在电脑上执行命令的AI”天然拥有高权限。生产级的Agent安全边界必须提前设计。Pi的思路是工具级权限控制给每个工具标注作用域比如文件工具只能在项目白名单目录内操作、命令工具默认禁止高危指令。从最小实现出发最简单的安全增强是给run_command加一道命令校验把rm -rf、sudo这类危险命令挡在外面或者在执行前打印即将执行的命令并要求人工确认。虽然会牺牲一些自动化程度但安全永远第一。4.4 上下文压缩与成本控制每次工具调用都会撑大上下文token成本水涨船高。在长任务里上下文压缩不只是“能不能塞下”的问题更是“成本能不能接受”的问题。Pi的上下文管理器会根据当前token量自动决定是否压缩历史消息。对我们的小实现来说一个简单可行的成本控制手段是限制工具返回内容的长度尤其是run_command的输出。有些命令的stdout动辄几百KB全部塞回上下文一次就能烧掉几千token。我习惯做尾部截断只保留前几百行和最后几十行因为报错信息通常集中在最后。5. 实操心得与常见问题排查踩过的坑和避坑清单5.1 我踩过的两个大坑写最小Agent的过程中我遇到最典型的问题有两个。第一个是工具返回消息的格式不对。OpenAI的Python SDK要求工具结果以roletool的消息追加content必须是字符串并且要带上tool_call_id。我当时把工具结果直接当普通assistant消息塞回去结果模型完全看不懂这些内容后续行为整个失控。排查了很久才发现是role和tool_call_id没对齐。第二个坑是循环终止条件写得太松。最初我只在模型不输出tool_calls时结束循环结果有一次模型连续输出十几轮工具调用而且每次去改同一个文件来回拉扯。后来加了max_steps上限还发现一个更聪明的辅助判断如果模型连续三轮都是同一个动作、参数也没变基本可以判定它卡住了主动终止并提示用户。这个“卡住检测”在生产级Agent里也是重要机制。5.2 常见问题速查表现象可能原因解决方法模型不调用工具直接输出代码工具描述不规范或系统提示词缺少引导强化工具描述在提示词中明确“必须调用工具执行任务”工具调用报400错误roletool消息缺失或tool_call_id不正确检查消息结构确保工具结果以roletool返回并携带正确IDAgent陷入死循环工具结果不满足模型预期或循环终止条件缺失设置max_steps检测重复工具调用并主动退出上下文溢出工具返回内容过长对长输出截断超过阈值时改用摘要模型接口超时模型服务未启动或负载过高先检查服务健康状态再配置超时重试5.3 给想上手的同学几点建议第一别急着上框架。先用200行代码把Agent Loop跑通一遍你对“工具、模型、循环”三者关系的理解会立刻不一样。第二把一个工具打磨扎实好过堆十个半残的工具——工具质量直接决定了Agent能完成的任务上限。第三上生产之前先写好日志和权限控制不要裸奔。你永远猜不到模型在下一条消息里会请求执行什么命令。最后说点我自己的心得。很多人以为Coding Agent的比拼重点是模型智商工程做久了会发现真正拉开差距的其实是工程骨架——谁能把不确定性管理好、把工具打磨得顺手、把成本控制在合理范围谁的Agent才真正能落地。模型是引擎骨架是底盘底盘不稳引擎马力再大也白搭。我自己写完最小实现、再对照Pi过完整个生产级框架之后最大的感觉是这个领域真还没到拼模型智商的时候大家比的是谁先把脚手架搭稳。如果你顺着文章把最小实现跑通了建议下一步给它加个“会话存档”功能——这个小练习会逼着你把消息结构和状态管理想清楚做完之后你再回头看Pi的session模块一定会会心一笑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑