发布时间: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 篇。