这个标题听起来像是一篇源码解读,但我想用更工程的方式来写这件事:真的从空目录开始,手写一个最小可用的 Coding Agent,然后拿 Pi 这个生产级 Agent 当参照物反复对照。之所以挑 Pi,是因为它属于 Codex CLI 这一代命令行 Coding Agent 里的典型代表,骨架暴露得很清楚,不像那些大框架上来就给你一堆抽象概念。整篇文章的目标是让你读完以后,能自己从 0 复现一个能跑的 Agent,并且清楚生产级 Agent 比"能跑"多做了哪些事。
1. 边界先行:Coding Agent 到底在解决什么问题
1.1 别把 Coding Agent 想得太玄学
Coding Agent 本质上就是一个"能自己调用工具的对话机器人"。它接收一段自然语言任务,比如"帮我写一个 Python 脚本统计当前目录所有 .py 文件的行数",然后自己做规划、执行命令、读取结果、调整方案,直到任务完成。这个过程和传统的自动化脚本有本质区别:传统脚本是提前写死的固定流程,每一步做什么都是人决定的;Agent 则是每一步都根据上一步的实际情况来做决策,决策者是大模型,而不是程序员。
用一个生活类比来解释:传统自动化脚本像一份详细到"第几步做什么"的遗嘱,Coding Agent 更像你请了一位外包员工,你跟他说目标,他去查资料、写代码、跑测试、遇到报错自己修,搞不定再回来问你。你只需要在关键节点把控方向,具体执行完全交给了对方。
这个"外包员工"的工作模式,就是 Agent 的核心循环(Agent Loop):模型生成一个决策,Agent 执行这个决策,把结果喂回给模型,模型再做下一个决策,循环往复。所有 Coding Agent,不管叫 Pi、Codex CLI 还是别的名字,骨架都是这个循环。不同的是生产级实现会在循环外面包上层层防护和基础设施,让它能长时间稳定运行、不把系统搞坏、不会丢失上下文。
1.2 为什么拿 Pi 当教学标本
我选择 Pi 作为参照物,有几个现实原因。第一,它是一个真正在生产环境里服役过的命令行 Coding Agent,用户通过终端输入任务,它直接操作文件系统、执行命令、生成代码补丁,功能链路完整。第二,它的代码结构比很多企业级框架干净得多,模块边界清晰,适合用来理解"哪一块代码解决了什么问题"。第三,这类工具普遍支持通过配置覆盖模型服务的 base_url,意味着你可以把它的后端接到任意 OpenAI 兼容接口上,这对想折腾本地模型或者不同云厂商模型的人来说非常关键。
我要坦白一点:这篇文章不会逐行贴 Pi 的源码,而是把它拆成骨架,带你先理解四个核心模块,然后亲手实现一个极简版,最后对照补齐。这样做完一遍之后,你再看任何 Agent 项目的源码,都会觉得眼熟。
2. 生产级 Agent 的骨架:Pi 是怎么被拼出来的
2.1 Agent Loop:所有 Agent 的心脏
Agent Loop 是整个 Coding Agent 最核心的部分,没有之一。它的运行逻辑非常简单,我写成伪代码你一眼就能看懂:
def agent_loop(task, memories): while True: 决策 = 调用大模型(任务, 工具列表, 历史记录) if 决策 == "任务完成": return 最终答案 if 决策 == "需要调用工具": 结果 = 执行工具(决策.工具名, 决策.参数) 历史记录.append(决策, 结果) if 达到最大轮数: return "超时未完成"为什么说它是心脏?因为整个 Agent 的智能程度被这个循环的稳健性限制住了。循环里每多一次交互,上下文历史就长一点;工具调用格式解析失败一次,整个任务就会中断;模型陷入死循环,这个 while 就会无限跑下去。生产级 Agent 的大量代码其实都在解决这些循环的边界问题:限制最大轮数、解析校验工具参数、截断过长的历史记录、处理模型返回的异常格式。
Pi 作为生产级 Agent,它的 Agent Loop 里还包含了不少额外机制:比如在每轮决策前做一次"规划摘要",让模型不至于被海量历史淹没;比如对长时间运行的任务做周期性总结,把旧对话压缩成要点;比如跳过一个工具的重复调用,避免模型反复请求同一个命令。
2.2 工具层:决定 Agent 能力的上限
工具层是 Agent 能"动手做事"的基础。大模型本身是纯文本模型,它不知道你的磁盘上有什么,也不能直接执行命令。工具层就是把模型和外部世界连接起来的桥梁。常见的 Coding Agent 工具包括:执行 shell 命令、读写文件、全局搜索、运行测试、创建 git 提交等。
在 OpenAI 兼容的接口里,工具通过 function calling 暴露给模型。你需要为每个工具定义一个 JSON Schema,说明这个工具叫什么、接收什么参数。模型在决策时会根据任务需要,返回一个结构化的工具调用请求,比如"调用 run_bash,参数是 ls -la"。Agent 收到这个请求后,执行真正的命令,再把执行结果作为一条 tool 消息返回给模型。
工具层的设计水平,直接影响一个 Coding Agent 能做什么。如果只暴露 run_bash 一个工具,它理论上能做任何事,因为 shell 就是万能接口;但完全放开 shell 也太危险,Agent 可能不小心执行 rm -rf。所以生产级 Agent 会在工具层做精细的权限管理:有的工具无条件允许,有的工具需要用户审批,有的工具完全禁用。Pi 的权限设计就采用了分级策略,危险命令和文件写入操作会弹出确认请求,普通只读命令则放行。
2.3 沙箱与执行环境隔离
执行环境隔离是生产级 Agent 和玩具级 Agent 最大的一道分水岭。你的极简 Agent 可以毫无顾忌地在宿主上执行 subprocess.run(command, shell=True),但生产级 Agent 绝对不会让模型直接在你的工作目录之外为所欲为。
Pi 这一类工具通常的做法是把每一个任务放到隔离环境里执行:要么是 Docker 容器,要么是受限的虚拟环境,要么是单独的临时目录,配合权限系统控制文件访问范围和网络请求。我见过不少踩坑案例,Agent 在执行一条测试命令时误伤了宿主机上的其他文件,或者生成的代码里有恶意脚本直接读取了用户 SSH 密钥。没有沙箱,这些风险全部裸奔。
对于自己在本地实验的情况,我建议至少做到三个隔离:用一个专门的临时工作目录当 Agent 的项目空间;不要让 Agent 直接使用全局 Python 环境,而是给它创建 venv;关键操作前先把目录做快照备份。这样就算 Agent 把项目搞乱,你也能随时恢复。
2.4 上下文管理:生产环境的隐形瓶颈
LLM 的上下文窗口是有限的,哪怕是最新一代模型,也可能在复杂任务中把上下文跑满。Coding Agent 的上下文消耗速度远比普通聊天恐怖:每轮决策要包含系统提示、用户任务、历史工具调用、工具输出结果。一条 grep 命令的输出可能就有几千 token,一个大型重构任务进行到一半时,历史记录轻松突破几万甚至十几万 token。
生产级 Agent 必须做上下文管理,常见手段有三种:截断、摘要、分层选择。截断最简单,直接把最早的历史消息丢掉,或者把过长的工具输出一刀切;摘要则是定期把前面一大段对话压缩成几百字的阶段性报告,替换掉原始消息;分层选择是把历史按重要性分级,比如系统提示和用户任务永远保留,工具输出可以被裁剪,中间过程和最终结论保留优先级最高。
Pi 的上下文管理逻辑里还有一个细节我很欣赏:它会给每个消息标记来源和用途,这样压缩时就有依据。相比之下,我们马上要写的最小实现只能靠粗暴截断,这个差距就是"能跑"和"生产级"的区别。
3. 从 0 写一个最小 Coding Agent:代码与拆讲
3.1 技术选型:为什么裸写不选框架
开始写代码之前,我决定不引入 LangChain、AutoGPT 这类框架,只依赖一个 openai Python SDK。理由很简单:框架会替你隐藏大量细节,而这次的目标恰恰是把细节看清。裸写出来的代码虽然简陋,但每一行都知道在干什么,后面你往里面加沙箱、加权限、加上下文管理时,才不会对着框架的抽象层不知所措。
依赖要求极低:Python 3.11+、openai 库。模型方面我建议先用支持 function calling 的模型,比如 gpt-4o-mini,它能在非常少的 token 预算内完成工具调用决策,跑起来便宜而且速度快。
3.2 最小 Agent 的完整实现
直接上代码,分三个部分。第一部分是工具实现:
import json import os import subprocess from openai import OpenAI def run_bash(command: str, timeout: int = 30) -> dict: """在 shell 中执行命令并返回结果,输出内容做硬截断防止上下文膨胀""" try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=timeout ) return { "return_code": result.returncode, "stdout": result.stdout[-4000:], "stderr": result.stderr[-2000:], } except subprocess.TimeoutExpired: return { "return_code": -1, "stdout": "", "stderr": f"命令执行超过 {timeout} 秒,已终止", } def read_file(path: str) -> dict: """读取文件,最多读前 4000 字符""" try: with open(path, "r", encoding="utf-8") as f: content = f.read() return {"ok": True, "content": content[:4000]} except Exception as e: return {"ok": False, "error": str(e)} def write_file(path: str, content: str) -> dict: """写文件,自动创建父目录""" try: full_path = os.path.abspath(path) os.makedirs(os.path.dirname(full_path), exist_ok=True) with open(full_path, "w", encoding="utf-8") as f: f.write(content) return {"ok": True, "path": full_path} except Exception as e: return {"ok": False, "error": str(e)}每个工具我都做了防御处理:命令执行加了超时,输出做了长度限制,写文件自动创建目录。这些看起来琐碎,但在 Agent 循环里每一条都可能是救命稻草,特别是输出截断,能直接避免上下文过早耗尽。
第二部分是工具 schema 定义和分发逻辑:
TOOLS = [ { "type": "function", "function": { "name": "run_bash", "description": "在 shell 中执行命令,获取输出。适合查看目录、运行脚本、安装依赖、执行测试。", "parameters": { "type": "object", "properties": { "command": { "type": "string", "description": "要执行的完整 shell 命令", } }, "required": ["command"], }, }, }, { "type": "function", "function": { "name": "read_file", "description": "读取指定文本文件的全部或部分内容", "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"], }, }, }, ] def dispatch(name: str, args: dict) -> dict: if name == "run_bash": return run_bash(args["command"]) if name == "read_file": return read_file(args["path"]) if name == "write_file": return write_file(args["path"], args["content"]) return {"error": f"未知工具: {name}"}schema 里 description 字段非常关键,模型靠它理解什么时候该用哪个工具。描述写得越具体,模型的工具选择准确率越高。比如 run_bash 的 description 里我特意写了"适合查看目录、运行脚本、安装依赖、执行测试",这是在给模型提示典型使用场景。
第三部分是 Agent 主循环,也是整个最小实现的核心:
SYSTEM_PROMPT = """你是一个运行在终端里的 Coding Agent。 你的任务是通过调用工具来完成用户交给你的开发任务。 规则: 1. 需要了解环境时,使用 run_bash 执行命令查看。 2. 需要查看或修改文件时,使用 read_file 和 write_file。 3. 你每轮最多调用一个工具,等拿到结果后再决定下一步。 4. 任务完成时,用一句话总结做了什么,不要输出额外的代码块。""" def agent_loop(user_task: str, max_turns: int = 8) -> None: client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1"), ) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_task}, ] for turn in range(max_turns): print(f"\n--- 第 {turn + 1} 轮决策 ---") resp = client.chat.completions.create( model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"), messages=messages, tools=TOOLS, tool_choice="auto", temperature=0.2, ) message = resp.choices[0].message messages.append( { "role": "assistant", "content": message.content or "", "tool_calls": ( [ { "id": tc.id, "type": "function", "function": { "name": tc.function.name, "arguments": tc.function.arguments, }, } for tc in message.tool_calls ] if message.tool_calls else None ), } ) if message.tool_calls: for tc in message.tool_calls: fn_name = tc.function.name try: fn_args = json.loads(tc.function.arguments) except json.JSONDecodeError: fn_args = {"error": "模型返回了无法解析的 JSON 参数"} print(f"调用工具: {fn_name}({json.dumps(fn_args, ensure_ascii=False)})") result = dispatch(fn_name, fn_args) messages.append( { "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False), } ) else: print(f"最终答复: {message.content}") return print("已达最大轮数,任务未完成。建议拆分子任务后重试。") if __name__ == "__main__": agent_loop("统计当前目录下所有 .py 文件的总行数,并写入 result.txt")这个循环的逻辑只有五个步骤:把系统提示和用户任务拼进 messages;调用模型;把模型返回的 assistant 消息追加到 messages;如果模型要求调用工具就执行并追加 tool 消息;如果模型直接给出最终答复就结束。整个实现大概 130 行,就是 Pi 这类生产级 Agent 的最简骨架。
3.3 关键机制逐条拆解:每一行都有理由
为什么每轮都要把 assistant 消息和 tool 消息追加进 messages?因为大模型本身无状态,它只能看到你传给它的全部消息。如果你不把上一轮的工具调用及其结果放回上下文,模型下一轮就不知道刚才发生了什么。这个"所有历史都在 messages 里"的机制,既是 Agent 工作记忆的来源,也是上下文膨胀的根源。
为什么设置 temperature=0.2?Coding Agent 的任务追求确定性,温度太高模型会"发挥创意"编造命令或路径。实测下来 0.2 左右能在保持准确的同时给模型一点点容错空间。完全设为 0 反而可能让解析函数调用时出现诡异失败,我踩过这个坑。
为什么 max_turns 设成 8?这是成本和质量之间的平衡。一个简单任务通常 3 到 5 轮就能完成,超过 8 轮往往意味着模型陷入反复试错。生产级可以设更高,但最小实现必须设上限,否则一个死循环可能烧掉你几十次 API 调用。
为什么要对工具输出做截断?一个 ls 命令的输出可能就有 5000 字符,而模型的上下文窗口不是无限的。截断到末尾 4000 字符是一种非常粗暴但有效的策略,因为命令执行结果的关键信息通常集中在末尾。生产级会做得更精细,但最小实现这样够用。
3.4 实测:让最小 Agent 完成一个真实任务
我在测试目录里放了三个 Python 文件,总计大约 150 行代码。然后把任务交给 Agent:"统计当前目录下所有 .py 文件的总行数,并写入 result.txt"。
第一轮,模型调用了 run_bash,执行 ls *.py 查看有哪些文件。我的工具返回了文件列表。第二轮,模型意识到还需要知道每个文件的行数,它调用了 run_bash 执行 wc -l *.py。工具返回了三个文件各自的行数。第三轮,模型决定不再调用工具,直接输出总结:"目录下共有 3 个 Python 文件,合计 157 行。已写入 result.txt。"我检查了一下 result.txt,内容正确。
整个过程 15 秒左右,API 成本不到一分钱。这就是一个最小可用 Coding Agent 的全部价值:它真的能自主完成一个小型编程任务,而你只需要给它一个自然语言目标。
4. 从最小到生产级:补上配置层和底座
4.1 把配置外置:环境变量与配置文件
最小实现里我直接用环境变量读 API Key 和 base_url,这对本地临时实验够用,但要想长期使用,你需要一个配置文件。Pi 这类命令行 Agent 的配置通常长这样,我用 TOML 示例:
[model] name = "gpt-4o-mini" provider = "openai" [api] base_url = "https://api.openai.com/v1" api_key_env = "OPENAI_API_KEY" [agent] max_turns = 8 sandbox = "venv" auto_approve = ["ls", "cat", "pwd", "find"]配置外置有几个好处:模型切换不用改代码;不同项目可以挂不同的 base_url;敏感信息通过环境变量注入,而不是硬编码在源码里。要注意的是 base_url 不是只能填 OpenAI 官方地址,凡是提供 OpenAI 兼容接口的服务都可以填进去,比如 Azure OpenAI、本地部署的推理服务、Ollama 等。我在本地实验时把 base_url 指向本地推理服务,模型就无缝切换成了本地模型,API Key 随便填一个占位符即可。这是这类 Agent 配置文件里最有价值的一个字段。
4.2 会话持久化与断点恢复
最小 Agent 的任务一旦中断,所有上下文全部丢失,下次启动只能从头再来。生产级 Agent 必须解决这个问题。实现思路也不复杂:把 messages 列表序列化成 JSON 存到磁盘,启动时如果检测到历史文件,就加载进来接着跑。
import json from pathlib import Path def save_session(task_id: str, messages: list) -> None: session_dir = Path(".agent_sessions") session_dir.mkdir(exist_ok=True) with open(session_dir / f"{task_id}.json", "w", encoding="utf-8") as f: json.dump(messages, f, ensure_ascii=False, indent=2) def load_session(task_id: str) -> list | None: session_file = Path(".agent_sessions") / f"{task_id}.json" if session_file.exists(): with open(session_file, "r", encoding="utf-8") as f: return json.load(f) return None这个机制的价值在于:Agent 跑了 20 轮快要完成任务时,如果 API 超时或者本地网络断了,你不至于前功尽弃。生产级的会话管理比这复杂得多,它还包含会话列表、多任务并发、历史压缩后的状态恢复,但基本原理就是"把上下文落盘"。
4.3 最小版本 vs 生产级版本的差距清单
我把这一路对照下来的差距整理成一张表,方便你按图索骥地给最小 Agent 打补丁:
| 维度 | 最小实现 | 生产级(Pi 这类) |
|---|---|---|
| 执行环境 | 本机直接 subprocess | Docker/受限容器隔离 |
| 权限控制 | 无,给 Agent 完全信任 | 分级审批、命令白名单 |
| 上下文管理 | 末尾硬截断 | 自动摘要、分层裁剪 |
| 会话持久化 | 无 | 会话存档、断点恢复 |
| 流式输出 | 一次性打印 | TUI 实时刷新 |
| 工具调用校验 | 基本 JSON 解析 | Schema 强校验、参数纠错 |
| 并发控制 | 无 | 文件锁、多任务队列 |
| 评测回放 | 无 | 历史 log 回放、eval 集 |
| 模型适配 | 仅 OpenAI SDK | Provider 抽象层 |
这张表里的每一项都是生产环境逼出来的问题。你自己动手做的时候,不必一步到位,按照实际需求逐项补就行。我个人的建议是优先补三样:沙箱隔离、权限审批、上下文压缩,这三样直接决定了你的 Agent 敢不敢真正用来干活。
5. 常见问题与排查技巧实录
5.1 模型不按工具调用格式输出
这是新手实现 Coding Agent 遇到最多的坑。现象是你已经传了 tools 参数,但模型却在 content 里直接输出"好的,我现在用 run_bash 执行 ls",而不是返回结构化的 tool_calls。
排查思路分三步。第一步,确认模型本身支持 function calling,有些轻量模型或微调模型不支持或者支持得不好;第二步,确认你的 tools 参数里每个 function 的 description 是否清晰,模型在不知道该调用哪个工具时更容易"说人话"而不是走结构化流程;第三步,如果仍然不按格式走,可以设置 tool_choice 强制指定工具,比如 tool_choice={"type": "function", "function": {"name": "run_bash"}},这会禁止模型跳过工具直接回答。
还有一个兜底技巧:在系统提示词里明确写"你必须通过工具获取信息后才能回答,禁止猜测量"。这个对大多数模型都有效,但不能完全依赖。
5.2 上下文过早被撑爆
工具输出太长是最常见的上下文杀手。除了前面提过的输出截断,还有两个有效手段:一是让工具返回"摘要式结果",比如 run_bash 默认只返回 exit code 和输出末尾 500 字符,但是如果模型需要看全量,必须显式传一个 full_output=True 参数;二是定期做对话压缩,当 messages 的总字符数超过某个阈值时,调用一个压缩函数把较早的历史消息替换成一段总结。
我在最小实现里选择硬截断 4000 字符,在多数场景下够用,但如果你要做长任务,建议实现下面的压缩逻辑:
def compress_messages(messages: list, keep_last: int = 6) -> list: """保留系统提示和最近 keep_last 条消息,把中间内容压缩成一条摘要占位""" if len(messages) <= keep_last + 2: return messages system_msg = messages[0] tail = messages[-keep_last:] compressed = { "role": "user", "content": "【以下是一段被压缩的历史记录】请基于现有上下文继续执行任务。", } return [system_msg, compressed] + tail这个逻辑简单粗暴,但能显著延长 Agent 的有效工作轮数。它也有副作用:被压缩掉的历史细节模型就看不到了,所以压缩前最好提示模型在最终答复里保留必要信息。
5.3 base_url 配置不生效或报错
配置 base_url 后常见的报错有三类:401 认证失败、404 请求路径不对、409 模型不支持。逐一排查:
- 401 通常是 API Key 不对,检查环境变量是否真的被代码读到,可以在代码里打印 os.getenv("OPENAI_API_KEY") 确认;
- 404 大概率是 base_url 路径问题,OpenAI 兼容接口的完整路径一般是 https://host/v1,注意如果服务商要求带 /v1 而你少写了,所有请求都会 404;
- 409 或者模型不存在提示,是 model 名字不对,不同推理服务对模型名要求差异很大,先看服务商文档确认准确的模型标识。
还有一个小技巧:配置好之后,先用 curl 直接请求一次该服务的 /models 接口,验证连通性和模型列表,再让 Agent 跑起来。这样能快速区分是网络问题、认证问题还是模型名问题,不用在 Agent 日志里瞎猜。
5.4 死循环与超时
Agent 卡在"反复调用同一个工具、拿到相同结果、再调用同一个工具"的循环里,是真实会发生的事。可能是因为模型上下文里缺少足够信息,也可能是工具输出被截断导致模型看不到关键内容,还有一种可能是任务本身超出了模型现有能力,它只能重复尝试。
我在最小实现里设置了 max_turns=8,就是为了兜底这种场景。但更优雅的处理是在循环里加"重复检测":如果模型连续三轮调用同一工具且参数完全一样,就中止循环并提示任务需要人工介入。还可以给每个工具的执行加超时,我用了 subprocess 的 timeout 参数,30 秒没跑完直接杀掉,这能有效防止 Agent 执行一个卡死的测试命令把整个进程挂住。
5.5 问题排查速查表
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| 模型不输出 tool_calls | 模型不支持 function calling / 提示词引导不足 | 换支持工具调用的模型;设置 tool_choice |
| 报错 "tool_calls is not JSON serializable" | assistant 消息格式不规范 | 手动构造 tool_calls 列表,去掉多余字段 |
| 上下文很快爆满 | 工具输出未截断 | 硬截断输出、压缩历史消息 |
| 401 / 403 | API Key 错误或过期 | 检查环境变量、换 Key |
| 404 | base_url 路径缺 /v1 | 补全路径后再试 |
| 任务完成了但结果文件为空 | write_file 路径没落盘 | 打印 write_file 返回值,确认路径 |
| Agent 反复执行同一命令 | 上下文信息不足或死循环 | 检查截断逻辑、增加重复检测 |
| 一条命令卡住整个 Agent | 没有超时机制 | 给 subprocess 加 timeout |
6. 做完最小实现之后,我最大的体会有三点
第一,Agent 的核心循环真的只有十几行代码,所有看起来高级的 Agent 产品,本质都是在这个循环外面叠加工程防护。所以当你再看到一个新的 Agent 框架,别被概念唬住,先找它的 agent_loop 在哪个文件,读那一百行你就懂了大半。
第二,裸写一次之后,你对任何框架的评价都会变得挑剔。比如有的框架把工具调用封装得极其复杂,实际跑起来反而不如直接操作 messages 来得直观。框架的意义在于帮你处理生产级的边界情况,而不是让核心循环变得更花哨。
第三,我现在所有 Agent 工具的第一优先级都是安全边界。最小版代码我全程只在临时目录里跑,因为这个 Agent 真的能执行任意 shell 命令,真的能覆盖你的任意文件。如果不加沙箱就拿到真实项目里跑,你早晚会遇到"Agent 把你的代码改崩了"或者"Agent 执行了危险的系统命令"这种事。我的建议是,在没做好权限隔离之前,只把它当玩具,别让它直接触碰重要目录。
最后分享一个操控技巧:想让 Agent 表现得更稳,就把一个复杂任务拆成多个小任务分轮执行,并在每轮开头明确告诉它"你现在只需要完成第几步,其他不要动"。这比让它在一次对话里从头做到尾的成功率高很多。我自己试过让 Agent 一口气写一个完整模块,结果它在中间状态里绕来绕去,后来改成"先写接口、再写实现、再写测试"三步走,每一步单独跑一轮,效果反而好得多。这个经验不仅适用于最小实现,也适用于生产级 Agent,算是这个项目留给我最实用的一条心得。