从 LangChain 到自研内核:Mistake Agent 开发记录
从 LangChain 到自研内核:Mistake Agent 开发记录
这个项目是怎么来的
我们打算进学校的一个项目组,考核项目里有一个"错题 Agent":给中学生用的本地学习助手,能上传作业自动批改、错题自动归档、按薄弱点出题。当时觉得挺简单,不就是调大模型 API 吗。
后来我才明白,这句话和"不就是写个操作系统吗"一样危险。
这个系列记录我从零搭一个 Agent 的完整过程。今天是序章:为什么做了两版、为什么第二版推倒重来、以及我把 Agent 当操作系统来设计的理由。
一个背景:全程 vibe coding,我是怎么拷打 AI 的
先交代一个背景:这个项目从头到尾都是 vibe coding 完成的。我没一行行手写,把需求、边界和"为什么"喂给一个叫 Codex 的 AI 编码 Agent,让它写代码,我负责拷打它。
所谓拷打,就是逼它把每个决策说清楚:
- 每个设计都要先讲"为什么"。看不懂就"展开说说",讲不清楚就换方案。比如"会话切换要不要甩给主模型",它给我讲了三次,我确认真懂了才拍板。
- 开放题全变成选择题。我不问"怎么办",而是让它给出 A/B 方案并附推荐理由,我来选。选择题比开放题好拷打一百倍。
- 改设计必须留痕。今天定的机制,明天它忘了怎么办?所以每个关键决策都要写进项目文档。改设计不留痕,等于没改。
- 测试必须接真实 API。不 mock、不假装,跑不通就是没做。模型报 503 要会重试,余额和缓存命中率这种数字,API 真返回了、真跑通了,我才信。
- 每完成一步就提交。小步提交、可回滚、可追溯,出了问题能精确指到是哪一步引入的。
- 定期复盘。我隔三差五让它"重新复盘已经落地的部分",并且明说"不要报喜不报忧"。
它当然也翻车:子 Agent 起失败、前端被我骂"你面向的到底是不是中学生"、把会话切换工具黏进上下文导致模型每轮都切会话。但翻车正是 vibe coding 的价值:AI 负责产出,我负责判断"这不对"。
还有一个背景:这个项目我只负责后端。内核、调度、模型接入、会话机制,这些是我和 AI 一起啃的;前端由团队里的同学负责做,而我和 AI 做出来的那个前端版本,一半是给她们做参考,一半是我自己拿来"当中学生"测产品的。前端要什么(Markdown 渲染、KaTeX 公式、XSS 防注入、图标统一、动画效果),全是我提的需求。后端是我负责写,前端是我负责"点菜"。
写到这里我发现一个呼应:这个系列要讲的 Agent,和我写这个系列时拷打的 AI,其实是同一种东西,都是"循环 + 工具 + 上下文"。区别只在于,我给 Agent 写的是代码,AI 给我写的是项目。至于"怎么拷打一个 AI",那其实是 Agent 开发里最像需求方的那部分技能。后面每一幕里的坑,很多就是这么拷打出来的。
两个词先对齐
在讲具体技术前,先对齐两个词,后面每一幕都会用到。
Agent(智能体):核心是一个循环。拿用户消息和当前状态去问大模型,大模型返回文字或者"我想调用某个工具";程序执行工具、把结果喂回去,大模型再继续,直到它认为任务完成。这个循环叫 agent loop,它才是 Agent 真正的骨架。市面上所有"Agent 框架",本质都是在给你这个循环的某一版实现。
LLM API:大模型本身不跑在你电脑上,你通过 HTTP 接口把"消息历史 + 工具清单"发过去,它流式返回文字和工具调用。2026 年的主流形态已经从 Chat Completions 演进到 Responses API:一次请求可以带消息、工具、输出格式,返回结构化事件流,思考过程和工具调用都是显式的事件。
如果这两点你已经懂,可以跳过。如果之前只写过"调接口拿一段文字",那这篇文章正好补上中间那块:一个 Agent 是一整个调度系统,不只是一次调用。
v1:Cuoti-Pro 是怎么来的
第一版叫 Cuoti-Pro(错题 Pro)。技术栈是 FastAPI + Vue 3 + Docker Compose,后端已经搭了一套"内核 + 插件"的雏形:内核负责鉴权、配置、数据库、审计日志、LLM 网关、Agent 运行时、RAG、知识图谱、文件存储;业务拆成插件,作业批改、错题本、掌握度追踪、分层练习、看板。还配了 role/ 分支、文档、接口规范,很"正规军"。
团队分工上,我只负责后端。FastAPI、内核插件、LLM 网关这些是我的活,前端和文档是别人的战场。
听起来挺完整对吧?问题恰恰出在"完整"上。
这是学校考核项目,多人协作、有评审节点。为了演示时"什么都能点",我们不停往内核里塞功能;为了部署省事,一切跑在 Docker 里;为了团队好分工,拆了一堆角色分支。到答辩时它确实是个"完整"的系统:能上传、能批改、能出报告、能演示。
但答辩结束的当晚,我就想重写。
不是哪个人错了。是合力把方向带偏了:演示优先的架构,和"真的有人长期用"是两套设计。这段经历我不会展开成八卦,它对我真正的价值是三条技术教训。
v1 的三条教训
教训一:演示优先,会让架构长出赘肉
评审导向意味着:功能广度大于深度,界面能点大于交互自然,能展示大于能维护。内核里什么都有,但每条链路都只做到"能跑"。等你想把某条链路做深,发现它被十几处耦合绑着,改不动。
后来重写时我给自己定了一条规矩:宁要三条能长期维护的链路,不要十三个能点一下的按钮。
教训二:Web 部署对中学生太重了
我们默认"网页版最方便",其实不是。
学生用户不会部署、不会配环境、甚至没有自己的服务器;家长的顾虑是隐私:作业和错题凭什么放在你的服务器上?Web 应用在这两个问题上是天生的输家。真正适合学生的形态是本地桌面应用:下载、安装、双击,数据全在自己电脑里。
教训三:框架替你做了关键决策
第一版用了 LangChain 一类的 Agent 框架。框架当然省事,但它把 agent loop、工具调用、上下文管理这些最核心的决策都替你定了,你只是在它给的缝隙里填业务。等你想改"工具调用失败怎么重试""上下文什么时候压缩""谁能调哪个工具",发现改不动,那不是你的代码。
提示
这里说的"框架"不特指 LangChain。任何 Agent 框架都一样:它在替你决定 Agent 的骨架,而你只有在它允许的范围内做选择。
重定义目标
推倒重来之前,我们把目标重新写了一遍:
| 原则 | 含义 |
|---|---|
| 本地优先 | 数据、密钥、错题本全在用户电脑上,无服务器、无云端同步 |
| 双击即用 | 桌面应用,安装完就能用,用户绝不碰命令行 |
| 面向中学生 | 用户是学生不是开发者,一切解释都要他们看得懂 |
| 长期维护 | 这是要陪用户几年的项目,不是一次演示 |
| 可扩展 | 新功能以插件形式长出来,不轻易动内核 |
第二版(Mistake Agent v2)就按这五条重写:Tauri 桌面壳 + Rust 自研内核,单 crate,AGPL-3.0 开源,仓库挂在 GitHub 上从第一天开始就公开。
为什么自研内核
既然 v1 的教训之一是"框架替你决策",那答案自然就是把 Agent 的骨架拿回来,自己写。
"自己写"不是从零实现注意力机制,模型还是调 API。要拿回来的是这些:
- agent loop:回合怎么开始、怎么结束、工具调用上限、超时、取消、失败重试
- 工具注册与调度:谁能调什么、参数校验、结果回填、调用审计
- 上下文与会话:消息树、分支编辑、上下文压缩、会话切换
- 信任边界:内核能碰什么、插件能碰什么,谁也不能越界
这些东西加起来,就是一个小操作系统。所以我干脆用操作系统的思路来设计:
- GUI 壳是用户态应用,唯一入口是结构化 RPC;
- kernel 是内核,管调度、审计、护栏;
- 内核插件(存储、记忆、验算、模型服务)是内核模块,在信任边界内;
- 业务插件(批改、练习、报告、考核、追踪)是用户态进程,只拿得到受限的"服务句柄",相当于操作系统的文件描述符,有权限才能碰资源。
框架替你决定 Agent 的骨架,自研内核让你自己就是骨架。
为什么用 Rust(以及 rustc 替我拷打 AI)
先声明:我不是"万物皆可 RIIR"的簇拥者。Rust 是慎重考虑之后选的,理由很具体:
- 新基座。项目从零开始,没有历史包袱,选一个还在快速演进的底座没有迁移成本。
- 活跃的社区。遇到问题能找到人、能找到库。
- 完善的类型系统。契约用类型写出来,非法状态在编译期就表达不出来。
- 成熟的检查机制。借用检查器、clippy、cargo test,一套磨合多年的把关体系。
- 尽量不把错误拖到运行时。这条对 vibe coding 尤其重要,下面细说。
rustc 替我拷打 AI
这个项目是 vibe coding:代码大部分是 AI 写的,我负责拷打。但说实话,AI 写代码最大的风险是"看起来能跑,其实运行时才炸"。Rust 的编译器恰好是最不知疲倦的那个评审:每一段 AI 生成的代码,都要过 cargo check、cargo test、cargo clippy 这一关。借用错、类型错、枚举漏分支,一律编译不过。AI 想蒙混过关,rustc 不答应。
所以我的拷打清单里有一条隐藏项:让编译器替我拷打一遍,我接着来。这比任何 prompt 约束都可靠,prompt 是建议,编译器是裁决。
当然编译器管不到的地方(架构对不对、体验顺不顺、会不会每轮切会话),还是我上场。rustc 管"代码对不对",我管"东西对不对"。
三个真实例子
类型即契约。工具的参数 schema 不用手写 JSON,直接从 Rust 类型用 schemars 派生;工具失败是结构化的 ToolError,带 code 和 retryable,loop 据此决定重试还是停手。契约长什么样,类型说了算。
可见性边界。单 crate 没有依赖图边界,能力边界就靠两层纪律:内核实现 pub(crate) 藏起来,插件只能拿类型化的服务句柄。编译期就拦住"插件直接碰文件系统"这种越界。
单二进制。内核直接跑在 Tauri 进程里,没有 sidecar、没有运行时依赖,一个可执行文件就是整个应用。这直接支撑了"双击即用"的产品目标。
为什么不是 Python
v1 是 FastAPI + Python。Python 写起来快,但"快"是把错误推迟到运行时的快:字段拼错、类型传错、结构不对,全要跑到那一行才炸。对 vibe coding 来说这是致命的,AI 产出本来就多,靠人肉 review 盯类型错误,盯不过来。Rust 把这一层检查前移到编译期,等于让编译器免费把这层检查做了。Go 和 TypeScript 也各有各的好,但对"一个长期维护的本地 Agent 内核"这个场景,Rust 的取舍最合我意。
三层架构
这张图是整个系列的地图。看不懂没关系,第一幕会把每一条边讲清楚,包括"为什么插件只能走服务句柄、不能直接碰文件系统"。
这个系列按项目的横切面分幕,全部幕次见侧边栏目录。
没有终章。项目还在长,等它真正"写完"那天再说。
仓库
- v2 当前仓库:kanaD3-Chan/mistake-agent
- v1 仓库:kanaD3-Chan/Cuoti-Pro
两者都是 AGPL-3.0。要是你也在折腾 Agent 项目,希望这个系列能帮你少踩几个坑。至少,下次想说"不就是调个 API 吗"的时候,能多想三秒。
