"你用了什么 AI 框架?""没有,我们自己写的。"
每次跟同行聊到薄荷智课——我们自研的 AI 教学管理助手——对方的第一个问题几乎都是:"你们用的 LangChain 还是 Dify?"当我回答"都没用,agent runtime 是自己写的",对方通常会愣一下。
这不是炫技。一年前我们也试过套用现成的 RAG+Chat 方案,用向量数据库存文档,套一个对话 UI,三天就能出 demo。但真正上线给教务老师用的时候,问题接踵而来:查学员课时余额要拼接三个数据源、排课需要检查教师和教室的时间冲突、续费分析要跨表聚合——这些不是"检索+生成"能解决的。
我们最终意识到:要做一个真正能用的 AI 助手,必须理解 agent 的底层运行机制,然后从这个理解出发去构建。这篇文章是这段构建过程的技术实录。
Agent 的本质:一个带工具的 while 循环
先祛魅。抛开所有包装,一个 AI Agent 的核心就是一个循环:把用户消息发给大模型,如果模型返回的是文本,输出给用户,结束;如果模型返回的是工具调用请求,执行工具,把结果喂回模型,继续循环。用伪代码写出来不到 10 行。
我们的实现在 agent.ts 的 chatStream() 方法中。它是一个最多 10 轮的 agentic loop:每一轮调用 LLM,检查返回结果——如果包含 tool_calls,就执行对应的工具,将工具返回值追加到消息历史,进入下一轮;如果是纯文本,流式输出给前端,循环结束。
这个循环虽然简单,但它是整个系统的心脏。理解了它,你就知道:为什么有时候 agent 会"绕圈子"(工具结果不够,模型反复尝试);为什么要设最大轮次(防止无限循环烧 token);为什么错误处理必须在每一轮都做(一个工具超时不能让整个对话崩溃)。
用框架的话,这些行为被封装在你看不到的地方。当 agent 行为异常时,你只能看日志猜。自己写 loop 的好处是:每一个分支、每一个 fallback、每一个终止条件,都是你写的,你能直接调。
Agent 不是魔法,是工程。理解了 agentic loop,你就掌握了 AI 产品的控制权。
工具调用的两种范式:逐个调用 vs. 编程式调用
标准的 tool calling 流程是:模型说"我要调 list_students",系统执行,结果返回模型;模型再说"我要调 get_attendance",系统再执行。每个工具调用都是一个完整的 LLM 往返。如果一个查询需要 5 次工具调用,就是 5 次往返,每次都要等模型推理 + 工具执行。
我们在实践中遇到了一个典型场景:老师问"这个月所有周一班级的出勤情况"。标准流程下,模型先调 list_classes 拿到所有周一的班级(假设 6 个),然后逐一调 get_attendance——6 次串行工具调用,6 次 LLM 推理,总延迟轻松超过 15 秒。这在产品体验上是不可接受的。
我们的解决方案叫 Programmatic Tool Calling(PTC)。核心思路是:与其让模型一次调一个工具,不如让模型写一段代码来调工具。模型生成一段 JavaScript,代码里用 call_tool() 函数批量调用工具,用 print() 输出结果。这段代码在一个沙箱化的 AsyncFunction 中执行。
还是上面那个例子,PTC 模式下模型会生成类似这样的代码:先调 list_classes 拿到周一的班级列表,然后用 for 循环逐一调 get_attendance,最后用 print 输出汇总结果。整个过程只需要一次 LLM 推理 + 一次代码执行,所有工具调用在代码执行阶段批量完成。延迟从 15 秒降到 3 秒。
安全性方面:PTC 的沙箱只暴露 call_tool() 和 print() 两个函数,没有文件系统、网络、进程访问。而且我们所有的 MCP 工具都是只读查询,即使代码写错了,最坏的结果也只是查到空数据,不会产生任何数据变更。代码执行设有 30 秒超时和 10KB 体积限制。
PTC 让 LLM 从「逐步执行者」变成「程序编写者」——这是我们架构中最重要的设计决策。
42 个 MCP 工具:工具设计的学问
MCP(Model Context Protocol)是 AI 模型调用外部工具的标准协议。对我们来说,MCP 的最大价值不是"标准化"这个抽象概念,而是两个非常具体的工程收益:第一,每个组织的 agent 会话运行在独立的 MCP 子进程中,通过 JWT 实现数据隔离——校区 A 的老师绝对看不到校区 B 的学员数据;第二,工具的增删不需要改 agent 核心代码,加一个新工具就是在 MCP Server 里注册一个函数。
我们目前有 42 个 MCP 工具,覆盖学员管理、班级管理、课程排期、考勤、成绩、线索跟进、运营仪表盘、财务课销、成长记录、知识库、问题反馈等模块。一个关键的设计约束是:所有工具都是只读的。我们不允许 AI 执行任何写操作——不能自动排课、不能修改学员信息、不能扣课时。这不是技术限制,而是刻意的产品决策。AI 查错了最多白查一次,写错了可能影响真实业务。
另一个我们花了大量时间的地方是工具的命名和 schema 描述。LLM 决定调用哪个工具、传什么参数,完全依赖工具的 name 和 description。一个叫 get_student_info 的工具和一个叫 get_student_balance_detail 的工具,模型的选择准确率差异巨大。我们反复迭代了工具描述的措辞,确保每个工具的职责边界清晰、参数含义无歧义。
这是很多人忽略的一点:工具设计的质量直接决定 agent 的准确率。模型本身的能力是一个上限,但工具 schema 的质量决定你能接近这个上限多少。
流式架构:让 Agent 感觉快的秘密
Agentic loop 有一个天然的体验问题:它慢。一次完整的交互可能经历 2-3 轮 LLM 推理和多次工具执行,总延迟 4-8 秒是常态。如果用户发完消息后看到一个转圈动画,等 8 秒才出结果,大部分人会觉得系统卡住了。
我们的解决方案是基于 SSE(Server-Sent Events)的多路复用流式架构。Agent 的每一个动作都会实时推送一个结构化事件到前端:tool_start(开始执行工具)、tool_end(工具执行完成,含耗时)、code_start/code_end(PTC 代码执行)、text_delta(模型输出的增量文本)、card(结构化数据卡片)、done(流结束)。前端根据这些事件逐步渲染 UI。
用户的实际体验是这样的:消息发出后,立刻看到"查询学员信息..."的工具执行提示(tool_start 事件);工具完成后提示变为"完成(0.8s)"(tool_end 事件);如果有结构化数据,数据卡片立刻渲染出来(card 事件),不用等后续文本生成;然后模型的分析文字逐字流入(text_delta 事件)。整个过程没有任何"空白等待"。
我们还设计了一个内置工具叫 __render_card。当模型判断返回结果适合结构化展示时(比如查某个学员的课时余额),它会调用这个内置工具生成一张数据卡片,包含关键指标的大字号展示、详细字段列表、以及后续操作建议按钮。卡片在模型生成阶段就通过 SSE 推送到前端,不需要等整个回答完成。
我们的规则引擎还会根据本次工具调用的模式,自动生成后续建议。比如查了考勤之后,建议"查看本周迟到学员"或"导出考勤报表"。这些建议以可点击标签的形式出现在回答下方,点击即发送——降低用户的下一步操作成本。
用户不在意 Agent 实际花了多长时间——他们在意的是「是否能看到它在思考」。流式渲染是体验的分水岭。
双端适配:Web 与企业微信的不同挑战
Web 端的流式体验做得再好,也解决不了一个现实问题:教培行业的一线老师和管理者生活在微信里。如果 AI 助手只能在浏览器用,使用频率会断崖式下降。所以我们同时支持企业微信作为入口。
企业微信有一个严格的约束:回调接口要求 5 秒内响应。而一次 agentic loop 可能需要 10-15 秒。我们的方案是异步处理:收到消息后立刻返回一个"处理中"的响应,后台启动 agent 执行,执行完成后通过轮询回调将结果推送回企微。虽然不如 Web 端的逐字流式那么丝滑,但至少做到了"发消息就能用"的零门槛体验。
两个入口的代码路径完全独立:Web 走 chatStream() + SSE,企微走 chat() + 轮询回调。共享的是底层的 Agent 类、MCP 工具层和 suggestion 引擎。这种分离让我们可以独立优化各端的体验,互不影响。
模型选择:不绑定单一模型的三层配置
我们的系统支持 14 个国产大模型——通义千问系列、DeepSeek 系列、智谱系列、Kimi——通过一个三层配置机制管理:平台层定义可用模型列表,组织层选择启用哪些模型并设默认值,会话层允许用户在对话时切换模型。这意味着一个大型培训机构可以给不同校区配置不同的默认模型。
不绑定单一模型是一个刻意的架构决策。国产大模型的迭代速度极快,每隔几周就有新模型发布。通过 OpenAI 兼容的 API 接口层,我们加一个新模型通常只需要在配置中注册,不需要改任何 agent 逻辑。这让我们能始终用上最好的模型,而不是被锁死在一个供应商上。
成本控制方面,我们做了 token 用量的按日聚合统计(按组织、用户、模型三个维度),并利用 prompt caching 减少了约 40% 的重复 token 消耗。对于教培这种利润率不高的行业,AI 的推理成本必须精打细算。
为什么不用现成框架?一个反直觉的答案
回到最初那个问题:为什么不用 LangChain 或 Dify?我们之前的文章甚至写过"小团队不应该从零构建 AI 基础设施"。现在我想修正这个观点。
准确地说:小团队不应该构建自己不理解的东西。但如果你理解了 agent 的核心——agentic loop、tool calling、streaming——你会发现这些东西的实现量并不大。我们的 agent runtime 核心代码大概 500 行。真正占工作量的是 42 个 MCP 工具的业务逻辑、前端的流式渲染、企微的适配——这些是任何框架都帮不了你的。
框架的问题在于:它帮你跳过了理解。当你需要实现 PTC(让模型写代码调工具)时,框架没有这个抽象;当你需要调试为什么某个工具调用准确率低时,框架的日志不够细;当你需要在 agentic loop 中插入数据卡片的即时推送时,框架的事件模型不支持。每一次"框架做不到",你都要在框架之上 hack,最终你维护的不是一个产品,而是一堆绕过框架限制的补丁。
对小团队来说,真正的风险不是"自己写太多代码",而是"用了工具却不理解工具在做什么"。一个三人团队里每个人都读过 agentic loop 的代码、都能调试流式事件、都理解工具调用的机制——这种理解深度本身就是产品的护城河。
小团队的优势不是「用更少的人做一样的事」,而是「每个人都深入理解系统的每一层」。框架帮你跳过理解,但理解恰恰是产品的护城河。
写给想做 AI 产品的技术团队
如果你的团队也在考虑做 AI 产品,分享三条我们踩坑之后的建议。第一,去读任何一个 agent 框架的核心 loop 代码,不管你最终用不用框架。大部分框架的核心循环不超过 200 行,读懂它你就理解了 agent 的本质,后续所有的架构决策都会更清晰。第二,从只读工具开始。AI 写数据涉及确认流程、回滚机制、权限校验等一整套安全问题,在产品早期没必要碰。只读查询已经能覆盖 80% 的高频需求。第三,先投资流式体验,再投资模型质量。一个实时反馈的普通模型,体验远好于一个让用户干等 10 秒的顶级模型。
薄荷智课目前服务多个校区的日常教学管理,42 个 MCP 工具覆盖了教务老师最常用的查询和分析场景。我们正在考虑将 MCP 工具层的部分实现开源,让更多教培机构能够在自己的系统上接入 AI 能力。
技术选型会过时,框架会更替,但对底层原理的理解不会贬值。如果这篇文章能帮你少走一段弯路,欢迎联系我们交流。
