Coding Agent 这个词最近被聊烂了,但大部分讨论都停在产品演示层面:你看它能自己建文件、跑测试、根据报错改代码,甚至能调 API 查文档。真正值得琢磨的是背后那套循环机制——模型怎么决定下一步、工具结果怎么反馈回去、什么时候才算“干完活”。我把玩过 Codex CLI 和 Pi 这类生产级 Coding Agent 之后,决定不满足于当用户,而是把它们的骨架拆清楚,然后从 0 徒手搭一个最小可运行版本。这篇东西就是那次拆解和重建的记录。
适合谁看?如果你知道 LLM API 的 chat/completions 长什么样,但一直没想明白 Agent 是怎么“自主”干活的,这篇文章能在半小时内帮你把概念落地成代码。就算你以后不写 Agent,理解这套循环,再去看各种 agent 框架的源码也会轻松很多。
1. 先搞清楚:Coding Agent 和普通 AI 编程助手到底差在哪
1.1 从“补全代码”到“自主干活”,中间隔着一个循环
很多人把 Coding Agent 和 AI 编程助手混为一谈,这其实是两类完全不同的东西。
AI 编程助手(比如各种 IDE 插件)的本质是补全。它看到你光标前面的代码,预测你接下来想写什么,本质上是把“上下文”当作输入,生成一个“局部输出”。它不会主动去读你的整个项目结构,不会替你运行测试,更不会根据报错信息回来改代码。
Chat 形态的编程对话(比如直接在对话框里贴代码问问题)比补全进了一步,但本质仍然是一次性的:你给它完整上下文,它给你一次回答。如果你让它改一个跨文件的代码,它只能依赖你手工贴上去的那部分——它自己没手去翻文件、跑命令。
Coding Agent 不一样。它的核心不是“生成一次答案”,而是在一个循环里持续决策、行动、观察、再决策。参考 Pi 和 Codex CLI 这类生产级 Agent 的实现,你会发现它们的运行方式惊人地一致:模型输出一个操作意图,系统执行这个操作(读文件、写文件、跑命令),把执行结果返回给模型,模型再根据新的信息决定下一步。这个循环跑上几轮,一个原本模糊的任务就变成了一个可验证的结果。
1.2 最小 Agent 三要素:大脑、手、反馈回路
我用一个不太严谨但很好用的类比来记这件事:把 Agent 想成一个新来的实习生。
- 大脑:LLM 本身。它负责理解任务、拆分步骤、判断下一步动作,但不直接动手。
- 手:工具层。实习生不能只靠脑子想象的代码去交差,他得能打开文件、保存文件、在终端里跑命令。对应到 Agent 就是
read_file、write_file、run_command这类工具函数。 - 反馈回路:实习生的主管,也就是系统本身。每一步工具执行完毕后,结果(文件内容、报错信息、测试输出)必须被回传到大脑那里,让大脑看到实际发生了什么,而不是想象发生了什么。
缺任何一个,Agent 都不成立。没有大脑,工具不会自己编排;没有手,模型只能空谈方案;没有反馈回路,模型改完代码不知道自己是成功了还是弄坏了——这本质上就是在写单轮对话。
1.3 为什么说“骨架”比“应用”更值得先研究
市面上的 Coding Agent 各有各的花活:有的做了代码库索引,有的接入了万级 token 的上下文压缩,有的支持多人协作审批流。但剥掉这些,底层骨架几乎都是同一个模型。拿 Pi 这种生产级实现来说,它可能用了更聪明的上下文管理、更完善的安全机制,但如果你把它的内部状态压缩到最小,看到的还是那三件事:模型、工具、循环。
这就是我为什么坚持先写一个最小实现的骨架——不是因为生产级的东西不重要,而是因为只有把骨架搞透了,你读生产级源码时才有坐标。你不会迷失在几十个文件里,而是能一眼认出:这个是上下文工程,这个是权限控制,这个是并发调度,它们都是长在骨架上的肌肉,不是骨架本身。
2. 核心循环拆解:模型决策、工具执行、结果观察怎么串成闭环
2.1 一次循环的四步伪代码
最小 Agent 的循环,用伪代码写出来不超过十行:
把系统提示词和用户任务拼成初始消息列表 重复最多 N 次: 1. 把整个消息列表发给 LLM 2. LLM 返回一个决定:要么调用某个工具(带上参数),要么直接给最终答案 3. 如果是调用工具: 执行对应的工具函数 把工具返回值作为一条新消息追加到列表里 回到步骤 1 4. 如果是最终答案: 返回给用户,循环结束这段流程里最关键的是第 3 步:工具返回值不是打印给人看的,而是作为上下文的一部分喂回给模型。模型在下一轮推理时会看到“我刚才读了文件,文件内容是……”,所以它能基于真实状态继续决策。这也是 Agent 和普通程序最大的区别——普通程序的状态存在变量里,Agent 的状态存在它的对话上下文里。
2.2 终止条件:什么时候才算“干完活”
循环必须有个出口,否则模型会无限调用工具。最小骨架里我用两个条件兜底:
- 模型主动说“我不调用工具了”。这是最理想的终止条件,意味着模型认为自己已经拿到了足够信息,可以交付最终答案。技术上通常通过 tool calling 机制判断:如果模型的响应里没有 tool_calls 字段,就把它当成最终输出。
- 步数超限。设一个最大循环次数(比如 10 步),防止模型陷入无限循环。生产级 Agent 的终止条件复杂得多,还包括人工确认、测试通过率、token 预算耗尽等,但最小骨架只需要这两个就够跑通。
实际调试时你会发现,终止条件的判断直接决定 Agent 的行为风格。如果你希望 Agent 改完代码必须验证,可以在系统提示词里明确写“修改代码后必须运行一次相关命令确认结果”,这样模型习惯在拿到结果之后再决定收尾。
2.3 “LLM 是概率引擎,Agent 是状态机”:对循环的另一种理解
如果你写过传统程序,可能会觉得这个循环很浪费——每一次都给模型传全部历史消息,又慢又费 token。但从另一个角度想,这个设计恰恰是 Agent 鲁棒性的来源。
LLM 本质上是概率引擎,同样的输入可能产生不同的输出,你没法保证它一次推理就能得到正确的工具调用序列。但当你把“决策-执行-观察”变成循环,每一步的错误都有机会被下一步纠正:模型读文件读漏了,看到执行结果不对,下一轮会重新读;模型写的代码有 bug,跑出报错信息,它可以根据报错修改。这就像一个普通人做事的方式:不是一次规划完美,而是边做边看、边看边调。
所以理解 Agent 的正确姿势是:把它当成一个状态机,状态 = 当前完整的消息列表,转移 = 一次工具调用及其结果。生产级 Agent 做的所有增强,本质上都是让这个状态机的状态更接近真实世界(代码库内容、运行环境),让转移更安全高效(权限校验、并发控制)。
3. 徒手实现:一个能改代码、跑命令的最小 Coding Agent
3.1 环境与依赖:一个 OpenAI 兼容客户端就够了
最小实现不需要任何 Agent 框架,只需要两样东西:一个能调 LLM 的 Python 环境,一个支持 tool calling(函数调用)的模型端点。
我用的是openaiPython SDK,因为目前大多数商用和开源的模型端点都兼容它的协议,改个base_url就能对接不同服务。这点在生产级 Agent 里也很常见——像 Pi 这类 CLI 工具,几乎都有配置项让你指定 API 地址,本质就是这里传的base_url。
pip install openai然后是 Agent 类的骨架:
import json import os import subprocess from openai import OpenAI SYSTEM_PROMPT = """你是一个运行在本地仓库里的编码代理(Coding Agent)。 你的工作方式: 1. 分析任务,决定需要哪些操作; 2. 调用可用工具修改文件或执行命令; 3. 观察工具返回结果,继续下一步; 4. 任务完成后,用一句话总结结果并停止调用工具。 可用工具:read_file、write_file、run_command。""" class MinimalCodingAgent: def __init__(self, base_url, api_key, model, workspace, max_steps=10): self.client = OpenAI(base_url=base_url, api_key=api_key) self.model = model self.workspace = os.path.abspath(workspace) self.max_steps = max_steps self.messages = [] self.tool_registry = {}workspace是 Agent 被允许操作的工作目录,这是安全边界的基础。所有工具在读写文件前都必须检查路径是否落在workspace之内。
3.2 三把“手”:read_file / write_file / run_command
我故意只实现三个工具,因为这三个已经能覆盖一个完整的编码闭环:看代码、改代码、验证代码。每个工具都是普通函数,但注册方式统一成一个结构,方便后续扩展。
def register_tool(self, name, description, parameters, handler): self.tool_registry[name] = { "handler": handler, "schema": { "type": "function", "function": { "name": name, "description": description, "parameters": parameters, }, }, }schema这一份会传给 LLM 作为工具定义,handler是模型说“我要调用这个工具”时真正执行的函数。这种注册模式在后面扩展新工具时特别清爽:新加一个函数、一段描述、一份参数 schema,循环代码一行都不用改。
然后是三个核心工具的代码。先看文件读写:
def _safe_path(self, path): full = os.path.realpath(os.path.join(self.workspace, path)) if full != self.workspace and not full.startswith(self.workspace + os.sep): raise PermissionError("path escapes workspace: " + path) return full def _read_file(self, path, offset=0, limit=80): full = self._safe_path(path) if not os.path.exists(full): return "ERROR: file not found: " + path with open(full, "r", errors="replace") as f: lines = f.readlines() return "".join(lines[offset:offset + limit]) or "(empty file)" def _write_file(self, path, content): full = self._safe_path(path) os.makedirs(os.path.dirname(full), exist_ok=True) with open(full, "w") as f: f.write(content) return "written " + path注意_safe_path里的判断条件:用workspace + os.sep而不是简单的startswith(workspace)。这是个很容易踩的坑,后面专门讲。_read_file带行数偏移和限制,是为了防止一次性读入超大文件把上下文撑爆。
再来看命令执行:
def _run_command(self, command, timeout=10): try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=timeout, cwd=self.workspace, ) output = (result.stdout or "") + (result.stderr or "") return (output or "(no output)")[:3000] except subprocess.TimeoutExpired: return "ERROR: command timed out after {}s".format(timeout)这里的cwd=self.workspace保证命令在项目目录里运行,timeout防止命令卡死,输出截断 3000 字符防止上下文膨胀。这三处细节,后面全是坑,生产级实现比我这里只会做得更严格。
最后把它们注册进工具表:
def _setup_default_tools(self): self.register_tool( "read_file", "读取工作区内某个文件的指定行范围内容", { "type": "object", "properties": { "path": {"type": "string"}, "offset": {"type": "integer", "description": "起始行号,默认0"}, "limit": {"type": "integer", "description": "读取行数,默认80"}, }, "required": ["path"], }, self._read_file, ) self.register_tool( "write_file", "在工作区内创建或覆盖写入一个文件,支持多级目录自动创建", { "type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string"}, }, "required": ["path", "content"], }, self._write_file, ) self.register_tool( "run_command", "在工作区目录下执行一条 shell 命令,捕获输出", { "type": "object", "properties": { "command": {"type": "string"}, "timeout": {"type": "integer", "description": "超时秒数,默认10"}, }, "required": ["command"], }, self._run_command, )工具描述写得越明确越好。LLM 不看你函数名和参数名,它只根据描述和你给的参数 schema 来决定怎么调用。描述里如果包含了“在你修改文件之后必须运行它来验证”这类引导,模型的行为会明显更可控。
3.3 主循环:把消息列表喂给模型,再消化工具结果
主循环是整个 Agent 的心脏。它做的事情很机械,但每个细节都对应前面说的循环模型:
def run(self, task, max_steps=None): max_steps = max_steps or self.max_steps self.messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task}, ] for step in range(max_steps): response = self.client.chat.completions.create( model=self.model, messages=self.messages, tools=[t["schema"] for t in self.tool_registry.values()], tool_choice="auto", ) message = response.choices[0].message if not message.tool_calls: return message.content self.messages.append(message.model_dump(exclude_none=True)) for call in message.tool_calls: handler = self.tool_registry[call.function.name]["handler"] args = json.loads(call.function.arguments) try: result = handler(**args) except Exception as e: result = "ERROR: " + str(e) self.messages.append({ "role": "tool", "tool_call_id": call.id, "content": str(result)[:3000], }) return "STEP_LIMIT: 达到最大步数仍未结束"几个值得注意的实现选择:
- 用 tool calling 而不是让模型输出一段 JSON。这是最小实现里最重要的决定。tool calling 是模型接口原生支持的机制,返回结果结构化、稳定,还能和消息一一对应。如果走文本输出 JSON 再自己解析的老路,模型格式错一处,整个 Agent 就断在那里(第六章会展开讲这个坑)。
- 每次循环都传完整消息列表。这看起来很浪费,但它是当前最简单可靠的方案——模型每一步都能看到全部工具执行历史。生产级会用各种手段压缩历史,但骨架阶段别自找麻烦。
- 工具异常要转成字符串返回给模型。函数抛异常不应该中断循环,而应该作为“工具执行结果”的一部分告诉模型。模型看到
ERROR: ...后会自己调整策略,这比直接崩溃要符合 Agent 的行为逻辑。
3.4 模拟一次完整对话:从“写一个 fib.py”到“答案是 55”
光看代码不够直观,我把一次真实运行的口径拆给你看。假设用户任务是:
在仓库里创建 fib.py,实现一个 fib(n) 函数并打印 fib(10) 的结果,然后运行它,告诉我输出是什么。
第 0 轮,模型返回一个工具调用:
{ "name": "write_file", "arguments": { "path": "fib.py", "content": "def fib(n):\n if n < 2:\n return n\n return fib(n-1) + fib(n-2)\n\nprint(fib(10))\n" } }系统执行后追加一条 tool 消息,内容是written fib.py。
第 1 轮,模型看到文件已创建,接着调用:
{ "name": "run_command", "arguments": { "command": "python3 fib.py" } }系统执行后追加55。
第 2 轮,模型发现任务所需的写文件和运行验证都完成了,不再调用工具,直接返回最终答案:
fib(10) = 55,已通过运行 fib.py 验证。
这个流程看着平淡,但它完整跑通了“意图 -> 执行 -> 观察 -> 再意图”的闭环。你把这个对话日志完整打印出来,就是 Agent 调试时最重要的第一手材料。
4. 给 Agent 装上“手”:工具层设计的四个关键细节
工具层是 Agent 最容易出问题的地方。模型本身不会犯错到“路径穿越”,但模型生成的字符串参数完全可能带出预期外的内容,工具层必须为模型的不确定性兜底。
4.1 路径沙箱:用真实路径校验防目录穿越
模型读文件时可能把路径写错,也可能是恶意提示词诱导它访问工作区外的文件。我在_safe_path里做两层校验:
full = os.path.realpath(os.path.join(self.workspace, path)) if full != self.workspace and not full.startswith(self.workspace + os.sep): raise PermissionError("path escapes workspace: " + path)第一层realpath是为了解析掉../和符号链接,拿到磁盘上的真实绝对路径。第二层判断用的是workspace + os.sep而不是startswith(workspace),原因很简单:/home/user/proj是/home/user/proj_evil的前缀,如果只用startswith,/home/user/proj_evil/secret.txt会被错误放行。加一个目录分隔符,就把这种“前缀碰瓷”挡掉了。
4.2 超时控制:命令可以跑,但不能无限跑
模型有时候会让 Agent 跑一个pip install或者长时间的测试脚本,如果没有任何超时保护,一次工具调用就能让整个 Agent 卡死。所以_run_command里必须带timeout参数。
选超时时间也有讲究:太短,正常的编译命令还没跑完就被切断,模型收到 timeout 报错会误判为失败;太长,死循环脚本会让 Agent 卡住。我的经验是默认 10 秒,同时把超时值暴露成工具参数,让模型在调用命令时根据任务类型自己指定更合理的值(比如跑测试传 30,改个配置传 5)。生产级实现一般还会限制同时运行的进程数,防止 Agent 一次性启动几十个命令把机器打爆。
4.3 输出截断:工具结果是上下文膨胀的元凶
工具执行结果会原样拼到消息列表里,如果某个命令输出 1 万行日志,下一轮请求的上下文就直接被撑爆或者烧掉大量 token。我在工具返回处统一[:3000]截断,看似粗暴,但非常有效。
更精细的做法是分层截断:对进度日志类输出只保留尾部(最新的错误信息通常在尾部),对文件内容选择“头部 + 行数统计”,对测试输出保留失败用例摘要。最小骨架阶段,一个全局截断加一个“文件太大只读前 N 行”的工具参数,足够应付绝大多数情况。等你看到生产级 Agent 里的 token 预算管理和上下文压缩算法,再回来对比这里,就能理解每个 fancy 设计都是在解决什么真实痛点。
4.4 并发与幂等:什么时候可以并行调用
LLM 一次响应里可能包含多个 tool_call(OpenAI 的接口允许模型在一条消息里请求并行调用多个工具)。骨架阶段我串行遍历,简单可靠。但你要知道在什么场景下可以放宽:
- 多个只读操作(读多个文件、查看 git 状态)天然适合并行,互不影响。
- 有依赖链的操作(先写文件再运行命令)必须串行,模型通常会把它们拆成两轮来避免依赖冲突。
- 写操作要幂等:同一个
write_file被模型重复调用不应造成污染——比如反复写同一份内容,结果必须一样。write_file天然满足,但“追加内容”这类工具就要小心,模型一旦重试就重复追加。
生产级 Agent 会维护一个依赖图和调度器,最小骨架里串行不丢人,重点是你得意识到这个扩展点在哪。
5. 从最小骨架到 Pi 这类生产级 Agent,中间补齐了什么
骨架跑通之后,我强烈建议你带着这个最小实现去读 Pi 这类生产级 Agent 的源码和文档。你会发现每个复杂特性都能对号入座到骨架的某个薄弱环节。这里列一张对照表,也是我拆解过程中的主要心得:
| 维度 | 最小骨架 | 生产级 Agent(Pi 这类参考) |
|---|---|---|
| 上下文来源 | 对话消息全量往返 | 代码库索引、git 状态、token 预算管理 |
| 工具数量 | 3 个基础工具 | 数十个子命令,支持工具组合与调度 |
| 任务执行 | 单个循环串行跑 | 规划层 + 执行层分工,可并行多路推进 |
| 安全边界 | 路径前缀校验 | 容器隔离、命令白名单、逐条人工审批 |
| 失败处理 | 异常转字符串喂回模型 | 自动重试、错误分类、checkpoint 恢复 |
| 可观测性 | 无 | 结构化 trace、每步 token 成本统计 |
5.1 上下文工程:从“全量对话”到“代码库索引”
最小骨架最大的瓶颈是:所有工具结果都塞在对话里,token 消耗随轮数线性上涨,很快撑爆上下文窗口。生产级实现会在几个方面做加强:
一是索引。让 Agent 在开始对话前先扫描仓库,建立符号、文件结构、依赖关系的索引。模型需要了解项目结构时,不需要傻读每个文件,而是查询索引。这像人进新公司先看组织架构图,而不是逐个工位打招呼。
二是摘要压缩。历史消息里过期的内容会被压缩成摘要,比如“用户已经要求修复了测试中的断言错误,期间改动过这两个文件”,保留关键信息但大幅减少 token。这块是生产级 Agent 工程含量最高的部分,也是各家拉开体验差距的地方。
三是定向召回。模型改某个函数时,只需要附近几十行的上下文,不需要整份文件。Tree-sitter 这类 AST 工具能精准切出函数体和调用关系,让模型“按需取用”。骨架阶段我们只能全量读前 80 行,这就是差距。
5.2 任务组织:Plan 与 Execute 分层
生产级 Coding Agent 普遍采用 plan-and-execute 模式:一个模型负责拆解任务、制定执行计划,另一个(或一组)模型按计划逐项执行。这比单循环的“边想边干”好在哪?
拆解出的计划能打破上下文瓶颈——规划阶段只需处理任务描述和项目概览,执行阶段每个子任务只接触自己相关的文件。计划还能支持并行:多个互不依赖的子任务可以同时推进。最重要的是,计划让 Agent 的整体行为可预测,你可以先让用户审阅计划,再决定要不要执行。
最小骨架里,这个分层体现在系统提示词上:我要求模型“先分析任务,决定需要哪些操作”。但这只是软约束,生产级会把“规划”做成一个独立的工具、一个单独的模型调用,甚至一个可编辑的计划文件。从软约束到硬流程,是生产级的典型进化路径。
5.3 安全与控制:审批、白名单、隔离
最小骨架里命令能跑任意 shell,文件能写任意路径(只要在 workspace 内)。这在真实场景里风险很大——你让 Agent 改代码,它可能因为提示词注入去执行危险命令(比如删库、上传敏感文件)。所以生产级会做三层控制:
- 命令白名单/黑名单。不是所有命令都让 Agent 自主执行,危险操作(如
rm -rf、git push)需要标记或拦截。 - 人工审批点。文件修改前 diff 给人看,关键命令执行前等人确认。这是 human-in-the-loop 的核心。
- 环境隔离。用容器、沙箱或虚拟机隔离 Agent 的运行环境,即使命令失控也不会波及宿主机。
最小骨架的_safe_path和timeout只是安全意识的雏形,真正要上生产,隔离和审批一条都不能省。
5.4 可靠性与可观测性:Agent 也要能事后复盘
普通程序崩溃了有栈信息,Agent“跑偏”了怎么办?生产级实现很早就意识到:Agent 是不可靠的,必须把它的每一步都变成可复盘的证据。所以它们会在内部记录完整 trace——每一步模型的输入输出、工具调用参数、耗时、token 消耗,全部结构化存储。
这个做法的价值在调试时体现得淋漓尽致。Agent 行为异常时,你可以像查日志一样按时间线回溯,定位是模型决策错了、工具执行失败了还是上下文被污染了。还能做成本分析:哪一轮最烧 token,哪个工具被无效调用最多次。最小骨架里我建议你至少加一行print日志,把每轮的工具调用打出来,这能省你大量排查时间。
6. 实测中踩过的坑:调试一个 Agent 比调试普通程序麻烦得多
写 Agent 的过程中,我踩了一堆“看着不大但很致命”的坑。这章把排查链路完整写出来,给你提供复现路径。
6.1 坑一:让模型输出 JSON 指令,结果十个字九个错
最早一版我不是用 tool calling,而是让模型直接输出一段 JSON,然后我自己解析。结果模型经常在 JSON 里加注释、加 markdown 代码块标记、字段名换行,正则都救不回来。更糟的是,解析失败后模型不知道发生了什么,下一轮还会照着错误格式再来一次,陷入死循环。
排查过程:我先 dump 模型原始输出,确认了问题出在格式不稳定;然后我切换到 tool calling 接口,让模型返回结构化的 function call 而不是自由文本。切换之后,解析问题彻底消失——tool calling 是模型原生训练过的输出格式,稳定性远高于让它“顺手写个 JSON”。
结论:能用 tool calling 就别让模型自由输出 JSON。这不是参数调优能解决的问题,是接口选型层面的正确决策。
6.2 坑二:工具输出太长,直接把上下文撑爆
第一次跑通 Agent 后我让它读一个大日志文件,_read_file一次性返回了 2000 行。下一轮 API 直接报 context length exceeded。排查发现,工具返回的长文本留在消息列表里,后面每一轮请求都会带着它,token 快速膨胀。
解决分两步:工具层统一截断输出(所有工具返回都限制长度),读文件工具加行数限制。从那以后我把这个当成工具层的默认规则——不要信任模型会控制输入,要在工具层替它把好关。
6.3 坑三:模型在同一步打转,反复写同一个文件
有一次 Agent 反复调用write_file写同一个文件,内容一模一样,每次工具都返回成功,但它就是不进入下一步。我加了逐步日志后发现,模型其实每次都想“先确认文件正确再运行命令”,但因为工具描述里写着“写入文件”,它一直把“写文件”当成唯一的进展手段。
修复方式很有启发性:我改写了工具描述和系统提示词,明确加上“写完文件后必须运行相关命令验证效果”。模型下一次调用链变成了“写文件 -> 跑命令 -> 看结果 -> 总结”,正常收敛。工具描述不是写给人看的注释,它是模型行为约束的一部分,这个经验在调 Agent 时特别值钱。
6.4 建立最小回归集:Agent 项目最值得做的一件事
Agent 是概率性的,你永远没法保证改一个提示词、加一个工具之后,原来能跑通的任务还跑得通。所以我强烈建议在一开始就维护一个最小回归集——三五个固定的任务,每个任务有明确的通过判定:
- 写一个文件,文件内容必须和预期完全一致。
- 改一个代码 bug,判定标准是测试命令退出码为 0。
- 读一个多级目录下的文件,判定标准是返回内容包含关键字符串。
每改一次 Agent 代码,就全量跑一遍回归集。刚开始会觉得烦,但 Agent 项目改动的连锁反应超出你的直觉判断,这个回归集是你在概率性系统里唯一的确定性锚点。
最后分享一点个人体会:从 0 到 1 搭出这个最小骨架,花费的时间远比想象中少,但收获远超预期。你不亲手写完那个循环,就永远在“理解”和“会用”之间隔着一层。等骨架跑顺了,再回头读 Pi 这类生产级实现的文档,你会看到每一处复杂设计都长在你踩过的坑上——没有这个骨架做参照,那些设计在你眼里就只是花哨的功能列表而已。