发布时间:2026-09-1
标签:AI Agent|工程实践|MVP|最小实现
架构画得再漂亮,也只是纸上的东西。
上一篇我画了四个节点:Task Router、Planner、Analyzer、Reviewer。听起来很完整,对吧?
但我决定,一个都不实现。
这一篇,我只写最窄的一条通路:用户问 → 调一个工具 → 读结果 → 回答。
原因很简单:我想看看,这个"最小可跑"的版本,到底会怎么死。
系列导航
- 上一篇:AI Agent 工程实践(38):从需求到 Agent 架构——为什么需要这些节点
- 下一篇:AI Agent 工程实践(40):第一次失败——Agent 为什么会做错
问题背景
这是第五阶段的第四篇,也是第一次真正写代码。
很多人会犯一个错:架构图里画了四个节点,就非得把四个都写出来才罢休。但我的经验是——先做一个故意很蠢的最小版本,让它跑起来,然后用它去暴露问题。
这个"最小版本"有个学名叫 MVP(Minimum Viable Product),但在这里,它的意义不是"证明能跑",而是"用最快的速度暴露它会怎么死"。
为什么暴露死亡这么重要?因为 Agent 项目最大的风险,不是"写不出来",而是"写了很多,但里面的假设全是错的"。你架构图里画的 Planner、Reviewer,可能根本解决不了真实问题——而这些问题,只有让最小版本跑起来、撞上真实问句,才会显现。
所以我这一篇,砍掉 Task Router、砍掉 Planner、砍掉 Reviewer,只保留一个 LLM + 两个工具(grep 和 read_file),让整条链路能跑通。
错误尝试
我差点犯了两个反方向的错。
第一个错:想一步到位。想把四个节点、六个工具、Memory、评估集全部写完再跑。结果就是写了一个月,一行能跑的代码都没有,还积累了一堆"我以为对的"假设。
这一堆"以为对的假设"是最贵的。比如我以为"LLM 会自己选对工具",直到真实跑起来才发现它经常选错;我以为"读到文件就能定位 bug",直到真实跑起来才发现它会编造不存在的函数。这些假设,只有让代码真的跑起来才会被证伪——而一步到位的写法,让你把所有假设都攒到最后一起爆。
第二个错:觉得太简单不值得跑。"一个 LLM + 两个工具,这有什么好跑的?" 但我告诉你,恰恰是这个最简单的版本,暴露了后面整整十篇要解决的问题。你只有真的跑起来,才能看到它选错工具、读错文件、凭空编造结论的样子。
两个错误殊途同归:都推迟了"第一次看到真实失败"的时间点。
这里我要特别强调一个心态:在 Agent 开发里,"先跑起来"的优先级,高于"架构正确"。因为 Agent 的行为极度依赖真实执行环境,你画在纸上的架构,有一半会在第一次真实运行时被推翻。与其花一个月搭一个"看起来正确"的架构,不如花两天搭一个"一定能跑"的最小版,然后用真实失败去修正架构。
关键观察
所以这篇的核心动作就一个:用最短的路径,让 Agent 第一次真正跑起来,然后诚实地记录它为什么不能直接上线。
最小版本长这样:
没有任何花哨的东西。一个循环:LLM 决定调工具 → 工具执行 → 结果塞回上下文 → LLM 继续,直到它觉得该回答了。
核心洞察:
MVP 的意义不是证明能跑,而是用最快的速度暴露它会怎么死。
这个"最小版"的价值,不在于它能做什么,而在于它把"Agent 运行的每一个环节"都摊开在你面前:LLM 决策、工具选择、参数传递、结果解析、终止判断——每一个环节都可能出问题,而这些问题,只有最小版能让你一个一个看清楚。
最终方案:100 行的最小 Agent
下面是 Repo Doctor v0 的完整实现,用 Python 手写一个 tool-calling loop,不依赖任何框架,就是为了看清每一环:
# repo_doctor/v0/main.py —— 最小可跑版本,约 100 行 import subprocess, json from openai import OpenAI client = OpenAI(base_url="https://api.deepseek.com", api_key="...") SYSTEM = """你是仓库诊断助手。你可以调用工具来调查代码库。 可用工具: - grep(keyword): 在仓库中搜索关键字,返回匹配的文件和行 - read_file(path): 读取指定文件内容 调查充分后,直接输出结论。""" TOOLS = [ {"type": "function", "function": { "name": "grep", "description": "在仓库中搜索关键字,返回匹配的文件和行", "parameters": {"type": "object", "properties": { "keyword": {"type": "string"}}, "required": ["keyword"]}}}, {"type": "function", "function": { "name": "read_file", "description": "读取指定文件内容", "parameters": {"type": "object", "properties": { "path": {"type": "string"}}, "required": ["path"]}}}, ] def call_tool(name, args, repo): if name == "grep": r = subprocess.run(["grep", "-rn", args["keyword"], repo], capture_output=True, text=True, timeout=10) return r.stdout[:3000] or "(无匹配)" if name == "read_file": with open(f"{repo}/{args['path']}", encoding="utf-8", errors="ignore") as f: return f.read()[:3000] return "(未知工具)" def run_agent(query, repo, max_steps=6): messages = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": f"仓库路径 {repo},问题:{query}"}] for _ in range(max_steps): resp = client.chat.completions.create( model="deepseek-chat", messages=messages, tools=TOOLS) msg = resp.choices[0].message if msg.tool_calls: messages.append(msg) for tc in msg.tool_calls: result = call_tool(tc.function.name, json.loads(tc.function.arguments), repo) messages.append({"role": "tool", "tool_call_id": tc.id, "content": result}) else: return msg.content return "(达到最大步数仍未完成)" if __name__ == "__main__": print(run_agent("这个项目里支付相关的逻辑在哪?", "/path/to/hello-agents"))跑一次,输出大概长这样:
结论:支付相关逻辑在 payment.py 中,核心函数是 payment_process(), 它负责订单支付和回调处理。建议查看该函数附近的 payment_callback()。这一版能跑。但先别急着高兴,我们仔细看这个结论——它有一个致命的问题:payment_process()这个函数,仓库里根本不存在。它是 Agent 编出来的。下一篇会专门解剖这个失败。
在进入"为什么不能上线"之前,先把这 100 行代码的每个关键环节点一遍,你就知道"最小版"到底暴露了哪些环节:
| 代码片段 | 环节 | 潜在问题(最小版就埋着) |
|---|---|---|
SYSTEM+TOOLS | 提示与工具描述 | 工具描述含糊,LLM 会选错工具(第 41 篇修) |
call_tool | 工具执行 | 参数不校验、结果截断 3000 字符(第 44 篇修) |
for _ in range(max_steps) | 终止控制 | 最大 6 步,可能"没查完就停"或"烧光"(第 41 篇修) |
messages.append | 上下文管理 | 无限增长,长任务爆上下文(第 45 篇修) |
return msg.content | 输出 | 结论无证据校验,幻觉直接进答案(第 40、42 篇修) |
这段 100 行代码,每一行都对应着后面一篇要解决的问题。这就是最小版的价值——它不是"最终产品的阉割版",而是"问题清单的具象化"。
架构图 / 流程图
把上面代码的执行流程画出来,你会看到它的"朴素":
看着很正常,对吧?但问题就藏在最后一步——那个payment_process()到底存不存在,Agent 根本没验证。
第二张图:这个循环的"隐患标注版"(发布提示:可用 draw.io 重画成正式图,与 Mermaid 图形成双图组合):
用户问句 │ ▼ ┌────────────┐ ① 工具描述含糊 → 可能选错工具(40/41 篇) │ Agent(LLM) │──────────────────────────────┐ └─────┬──────┘ │ │ ② 参数不校验 → 可能传错参数(41 篇) │ ▼ │ ┌────────────┐ ③ 结果截断 3000 字符 │ │ call_tool │ 可能丢关键信息(44 篇) │ └─────┬──────┘ │ │ ④ 结果塞回上下文,无校验 │ ▼ │ ┌────────────┐ ⑤ 结论无证据检查 │ │ Agent(LLM) │ 幻觉直接进答案(40/42 篇) │ └─────┬──────┘ │ │ ⑥ 输出结论 │ ▼ │ 答案 ◄───────────────────────────────────┘ (payment_process() 可能是编的!)这张图把"最小版"的每一个薄弱环节都标了出来。后面整整十篇,就是逐个把这些"隐患标注"换成"已修复"。
为什么不能直接上线
这一版跑通了,但我把它能跑和能上线分得很清。下面这张清单,就是我"故意留的技术债",也是后面整整十一篇的伏笔:
| # | 技术债 | 后果 | 后面哪篇解决 |
|---|---|---|---|
| 1 | 没有 Task Router,三种任务混在一起 | 该定位时去解释 | 47 |
| 2 | 没有 Planner,"查够了没"全凭感觉 | 草率下结论 / 烧 Token | 40、41 |
| 3 | 工具描述太模糊,参数没约束 | 选错工具、传错参数 | 41、42 |
| 4 | 没有 Reviewer,结论无证据链 | 幻觉成灾,payment_process()可能不存在 | 40、42 |
| 5 | 没有 State,调查过程不记录 | 无法回溯"为什么这样想" | 42 |
| 6 | 没有评估集,改完好坏不知道 | 越改越玄学 | 43 |
| 7 | 上下文无上限,读文件会撑爆 | 长文件直接溢出 | 44、49 |
| 8 | 出错就死,无兜底 | 工具报错整个流程崩 | 44 |
| 9 | 无法复现(模型/Prompt/工具都没锁版本) | 同一个问题两次答案不同 | 45 |
| 10 | 没有成本/延迟监控 | 烧多少 Token 全靠猜 | 46 |
这一篇的价值,就是把上面这十个坑提前摆在明面上。它们不是"以后可能会出问题",而是"现在就已经埋下了,只等真实问句来引爆"。
设计权衡
| 候选方案 | 优点 | 缺点 | 为什么不选 |
|---|---|---|---|
| 一步到位写完整架构 | 一次成型 | 一个月跑不起来,积累一堆未验证假设 | 推迟了看到真实失败的时间 |
| 用 LangGraph 框架搭 | 省事、规范 | 掩盖底层细节,出了问题看不懂 | 先手写 loop,看清每一环 |
| 100 行手写最小版 | 快、透明、暴露问题 | 功能残缺 | 最快暴露它会怎么死 |
关于"为什么不用 LangGraph"值得单独说:不是 LangGraph 不好,而是在这个阶段,你需要的不是框架的便利,而是对每一环的可见性。手写 loop,你能精确看到"LLM 这次调了什么工具、传了什么参数、工具回了什么"。等这套东西你想清楚了,第 49 篇再谈要不要换成框架。
常见误区(FAQ)
Q1:MVP 越少越好,是不是连工具都只留一个?
最少两个。一个工具(比如只留 read_file)无法暴露"工具选择"这个环节的问题——而工具选错恰恰是 Agent 最常见的失败之一(第 40 篇的 Tool Selection Error)。
Q2:为什么不用 LangGraph 搭 MVP?
对初学者来说,框架会掩盖"每一环发生了什么"。手写 100 行 loop,你被迫面对工具调用、参数解析、结果回填这些最底层的问题——这些问题在框架里被封装了,你直到出 bug 才知道它们存在。
Q3:这 100 行代码最后会被扔掉吗?
不会全扔。它的"工具调用循环"骨架会被保留,后续的 Task Router、Planner、Reviewer 都是在这个骨架上"加节点",而不是推倒重来。MVP 不是一次性用品,是后续版本的"可运行的基线"。
Q4:怎么判断 MVP"够了"?
一个简单标准:它能完整跑完一次端到端任务(哪怕结果错误)。能跑 + 会错,就是最好的 MVP 状态——因为下一步就是去解剖"它怎么错的"(第 40 篇)。
总结
✅ 先做最小可跑版本,故意砍掉所有"高级节点"。
✅ 100 行手写 loop,就是为了看清每一环。
✅ 这 100 行里,每一行都对应一篇后续要解决的问题。
✅ 这一版能跑,但埋了 10 个技术债。
✅ 铁律:MVP 的意义不是证明能跑,而是最快暴露它会怎么死。
✅ 下一篇,让这个最小版跑真实案例,看它第一次做错。
参考资料
- OpenAI Function Calling 文档 → 为什么引用:tool-calling loop 的 API 用法,是这 100 行的基础。
- 《The Pragmatic Programmer》"tracer bullet" 概念 → 为什么引用:先打通一条最小链路再扩展,正是本篇的方法论来源。
系列导航
- 上一篇:AI Agent 工程实践(38):从需求到 Agent 架构——为什么需要这些节点
- 下一篇:AI Agent 工程实践(40):第一次失败——Agent 为什么会做错
本文是 [AI Agent 工程实践] 系列的第 39 篇。