起因很简单:我天天用 Claude Code、配过 MCP、跑过 OpenClaw,手里的工具一个比一个高级,却从没自己从零写过一个 agent。我想知道这层包装底下到底是什么。
三天后我做出来了——一个两百多行的 CLI agent,能读写文件、跑 shell、压缩上下文、记忆权限。代码不复杂。但做完之后我对 agent 的判断变了:它的门槛根本不在算法,全在工程。 核心循环十几行就写完,难的是这个循环之外、为了让它在真实负载下不崩,要处理的那一堆边界条件。
下面是这三天里,几个真正改变我认知的点。
一、为什么不用框架
LangChain、LangGraph、CrewAI 我都跑过 demo,半小时就能出一个像模像样的 agent。但那半小时学到的是这个框架的 API,不是 agent 本身的工作原理。我要的是后者。
更实际的理由是可观测性。框架替你做了太多事,一旦出问题,你分不清是模型没接住、框架没传对、还是工具没装好。两百行自己写的代码,每一行报错我都知道该看哪。这个项目后面撞的那些坑,有一半是因为我能看见每一层在干什么才定位出来的——用框架的话,它们会变成一句看不懂的栈回溯。
二、几个边界,自己给自己画的
只做 CLI,不做 GUI。 一开始想做个 Codex 桌面版,后来放弃了——Electron 打包、跨平台、窗口渲染,光配置就能耗掉一周,等 GUI 跑通,agent 本体一行没写。把全部注意力留给核心逻辑,这个取舍后面被证明是对的。
模型走 DeepSeek 的 Anthropic 协议端点。 便宜、国内稳。它同时支持 OpenAI 和 Anthropic 两种格式,我选了 Anthropic——tool_use 是 content 里的一个 block,工具结果用 tool_result 塞回去,整个交互统一在一个结构里,换模型只改 base_url。这个选择后面带来了一个意外的代价,下面会讲。
拆成 v0→v3,每个版本能跑通才进下一个。 这是让我真正做完的关键。v0 对话循环、v1 文件读写、v2 shell 执行、v3 会话持久化,每个都是一小时能搞定的事。比起"做一个 agent"这种没有边界的目标,"今晚把 read_file 跑通"是大脑能启动的任务。
三、核心就是一个 while 循环
写完回头看,agent 这个被各种 PPT 包装得很玄的东西,骨架就这十几行:
while True:
response = client.messages.create(model=MODEL, messages=history, tools=tools)
history.append({"role": "assistant", "content": response.content})
if response.stop_reason != "tool_use":
break # 模型说完了
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result,
})
history.append({"role": "user", "content": tool_results})
Claude Code、Codex、Cursor 的 agent 模式都是这个骨架。值得记住的有三点:
while 是 agent 和 chatbot 的本质分界。chatbot 一问一答,agent 循环到任务完成——你发一句话,它可能内部调五次工具、转八轮,全程自己跑。
stop_reason 是唯一的终止信号。这一点我后来踩了坑:早期我写成"必须有 text 块才继续",结果模型某轮只返回 tool_use 不带 text,循环直接抛 StopIteration 崩了。正确做法是只认 stop_reason,文本有没有都不影响循环推进。
工具执行在你的代码里,不在模型里。模型只是输出"我想调 run_bash,参数是 ls -la"这个意图,真正的 subprocess 是你的代码跑的。模型与外部世界之间,隔着你写的这一层——这层的健壮性,决定了整个 agent 的健壮性。
到这里,"教程级"的 agent 就完成了。但真正的工程,是从我让它干第一件真活开始的。
四、让它干真活之后,撞到的五个工程约束
我给它的任务是:写一个完整的 Three.js 小游戏。一个需要生成几千行代码、跨十几轮工具调用的任务。然后这两百行代码被按在地上反复摩擦,逼出了五个我此前没意识到的约束。
1. agent 的"退出"比"执行"难写
第一个让整个会话彻底卡死的,是一个 400 错误:某个 tool_use 块后面没有配对的 tool_result。
根源是我自己埋的:我加了"连续空参数就智能终止"的保护,但终止时模型已经发出了 tool_use,我却直接掐断循环、把控制权交还用户,留下一个没有结果的孤儿块。Anthropic 协议要求二者严格配对,于是之后每一次 API 调用都因为这个不合法的 messages 报 400——会话从此死锁,发什么都一样。
这让我意识到 agent 的状态机里有一类隐藏的不变量:任何中断路径,都必须维持上下文的协议完整性。执行逻辑只要顺着走就行,但退出逻辑要在任意一个中断点——用户拒绝、异常、超限、死锁检测——都保证 messages 合法。这和数据库事务无论在哪一步失败都必须回滚到一致状态,是同一个问题。最后我加了一个不变量守卫:每次调 API 前扫一遍 messages,任何没配对的 tool_use 自动补齐 tool_result。这道防线让"协议不合法"这类错误从根上不可能发生。
2. 有些"bug"是模型的能力边界,不是你的代码问题
修完配对,问题没消失:模型反复调用 write_file,但传进来的参数是空的 {}。我一度以为是自己解析 tool_use 时丢了字段,加了原始响应打印之后才看清——模型返回的 block,input 字段本身就是空的。
规律很清楚:让它写一万字符左右的分片,每次都成功;让它"一次性生成整个大文件",input 就吐空。这不是我能用代码修好的 bug,是 DeepSeek 在生成超大 content 的 tool call 时的能力上限。
这个区分很重要。新手会一直在自己代码里找原因,把时间耗在一个根本不在你这边的问题上。正确的反应是承认这是外部约束,然后设计一个让模型绕过自身限制的机制——而不是指望模型变强。下面两条都是这个思路的延伸。
3. 为什么没有任何成熟 agent 用"整文件写入"
知道模型一次写不下大文件后,我让它分片写。结果第二片把第一片覆盖了——我的 write_file 是覆盖写。模型于是卡在一个死结里:一次写不下,分片又互相覆盖,两条路都走不通。
解决它的过程,让我反推出了一个之前没想通的设计:Claude Code、Cursor、Aider 这些工具,改文件从来不是重写整个文件,而是用精确替换(str_replace / diff)。 我一直以为那只是为了省 token,现在才明白更深的原因——它从根本上规避了"模型生成超大 content 会失败"这个约束。改一行就只发一行的 diff,永远不会触碰生成上限。我先用 append 模式救了急,但真正的答案是 str_replace。这是整个项目里我最有收获的一个反推:一个看起来像"优化"的设计,背后往往是在绕开某个硬约束。
4. 防死循环:要区分"在推进"和"在打转"
最早我给循环设了 10 轮硬上限。但真实任务很快暴露它的粗糙:复杂任务十轮根本不够,而陷入死循环时,这十轮又是白白烧掉的钱和时间。
固定轮数是个糟糕的信号,因为它衡量的是"跑了多久",而不是"有没有进展"。我把它换成了进展检测:轮数上限放宽到 50(作为最终安全网),同时记录最近几轮的工具调用,一旦发现连续多次"同一个工具 + 同样的失败",立刻终止。这样真正在推进的任务(每轮读不同文件、写不同内容)能跑很久,而原地打转的死循环两三轮就被掐断。Claude Code 这类产品的"自动停止"也是这个逻辑——不是数轮数,是判断有没有在前进。
5. 权限的粒度,是便利和安全的权衡
我给 run_bash 加了"始终允许"的记忆功能,第一版用完整命令做精确匹配——结果几乎没用。因为 agent 干活时几乎不会执行两条一模一样的命令,每一步都是新命令,"始终允许"永远命中不了,我被无限弹窗。
这暴露了权限设计的核心矛盾:匹配粒度太细则毫无便利,太粗则失去防护。 我最后做了分层:可以按"命令主程序"批准(信任所有 node 命令),也可以"本次会话全部允许"(重启失效),但危险命令黑名单的优先级永远最高,盖过一切批准。粒度的选择本质是在回答"我信任到什么程度"——是信任这一条命令、这一类工具,还是这一整段时间。没有标准答案,只有针对场景的权衡。这一层思考,比那个功能本身值钱。
五、做完之后,我对 agent 的判断
核心循环十几行,但要让它扛住真实负载,围绕这个循环要搭的东西——协议不变量守卫、模型能力边界的兜底、精确编辑、进展检测、分层权限——是几百倍的工作量。
这也是 Claude Code、Codex 卖得动的原因:它们把这些工程坑全填完了。网上能搜到的 agent demo,十个有九个会死在这些坑上,因为 demo 只跑顺路径,而这些坑全在异常路径和边界条件里。
但理解了核心循环、又亲手踩平这些坑之后,再看那些产品,我看到的不是"AI 有多神",而是"它们在每个工程点上做了什么取舍"。祛魅之后剩下的,是判断力——知道一个 agent 难在哪、好的设计为什么那样设计。这是这次手写最大的收益,比那个能跑的程序本身重要得多。
项目地址:https://github.com/SJCZL/minicodex