手写一个最小 Agent:5 个模块、1 张流水图、3 道生产坎,看完就能上手

📅 2026/7/28 6:04:52
手写一个最小 Agent:5 个模块、1 张流水图、3 道生产坎,看完就能上手
“LangChain、LangGraph 都别提了。给你白板5 分钟画出一个能跑的最小 Agent然后我会用一个真实业务问你它怎么落地。”Agent 的难点从来不是写一个 while 循环而是把循环里的几件事拆干净——拆得每一块都能独立换、独立测、独立扩。这篇文章不堆代码只讲架构。我会带你做三件事拆把最小 Agent 切成五个模块每块管什么、为什么独立、什么时候它会不够用串用一个真实的电商客服订单查询场景把五个模块从用户输入到最终回答跑一遍扩业务上线后必然遇到的三个真实问题持久化、上下文超限、用户自定义能力逐一对应到模块的扩展点读完你应该能在白板上 5 分钟画出五个框再用 5 分钟告诉面试官客服 Agent 是怎么从这五个框里长出来的。一、贯穿案例小明的订单为什么还没到先把这个案例摆在最前面后面五个模块全部围绕它展开。某电商平台用户小明发来一条消息“我上周买的耳机怎么还没到订单号是 2026052001能帮我看一下吗还有如果今天还到不了我想退款。”这一句话里藏了两个工具调用、一个条件判断和一次自然语言回复• 调query_order(order_id)拿到物流状态• 根据状态判断是否要调request_refund(order_id)• 把结果用人话讲给小明如果你用纯 Workflow 写要预先画清楚所有分支——但用户的问法千变万化下一个用户可能问我那笔退款到账了吗或帮我把上周三那个订单地址改一下。用 Agent 的理由就在这里让 LLM 在运行时决定调哪个工具、什么顺序、什么时候停。带着这个场景往下看。二、五个框最小 Agent 的架构总览先上图再解释为什么是这五个┌─────────────────────┐ │ AgentLoop │ 主循环串起所有人 │ while not done: │ └──────────┬──────────┘ │ ┌──────────────────────┼──────────────────────┐ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ AgentState │ │ LLMClient │ │ ToolExecutor │ │ 消息步数 │ │ 推理决策 │ │ 解析执行 │ └──────────────┘ └──────────────┘ └───────┬──────┘ │ ┌──────▼───────┐ │ ToolRegistry │ │ 工具登记表 │ └──────────────┘模块一句话职责在订单案例里负责啥ToolRegistry工具的登记处 执行入口登记query_order和request_refund的函数和 SchemaAgentState状态容器纯数据装小明的原话、LLM 的中间决策、工具返回值LLMClientLLM 的统一入口把消息和工具描述喂给 GPT/Claude/本地模型ToolExecutorTool Call 的解析与执行调度拿到 LLM 的我要调 query_order真去调AgentLoop编排所有人的 while 循环决定什么时候继续推理、什么时候停为什么是这五个不是三个或八个因为这是职责能否被独立替换的最小切分。少一个扩展时就会改到不该改的地方多一个就在制造没必要的抽象。下面一个一个看看的时候我会刻意换不同的讲法——有的从反例代码起手有的从设计模式切入有的直接走数据流。三、ToolRegistry先看一段反例代码很多新手第一次写 Agent会写出下面这种东西# 反例工具调用混在主循环里whilenot done: response llm.chat(messages) if response.tool_call: name response.tool_call.name args response.tool_call.arguments if name query_order: result query_order(**args) elif name request_refund: result request_refund(**args) elif name change_address: result change_address(**args) # ... 又来一个工具再加一个 elif messages.append({role: tool, content: result}) else: return response.content这段代码上线一周后会变成什么样——几十个 elif 堆在主循环里每加一个工具改一次 AgentLoop。这是教科书级别的开闭原则违反。ToolRegistry 干的就是把这堆 elif 抽出去• 注册时函数本体 给 LLM 看的 Schema 一起存进去• 执行时按名字查表传参调用返回任何工具的异常都包成结果返回不让它炸到主循环回到订单案例注册一次后registry.register(query_order, query_order, schema...)registry.register(request_refund, request_refund, schema...)主循环里只剩一句result registry.invoke(name, args)——再加 50 个工具也是这一句。一个细节面试爱追问工具抛异常该不该终止 Agent答案是不该。把异常信息当成 tool result 喂回去让 LLM 自己决定要不要重试、换工具或者放弃。这一步如果做错Agent 的可恢复性立刻归零。四、AgentState为什么状态必须是一个对象这一节我换个角度——从如果不独立会怎样讲起。假设你把 messages 直接写在 AgentLoop 的局部变量里def run(user_input): messages [...] step 0 while step 10: ...短期看没问题。但生产环境很快会冒出三个需求小明今天聊到一半下线明天回来要接着聊 →持久化小明上次说过我喜欢顺丰别给我发圆通 →跨会话记忆Agent 跑了 8 步崩了运维要从崩溃点恢复 →Checkpoint每加一个需求都要改run()函数。三次之后run()就成了垃圾场。把状态抽成AgentState对象之后分工立刻清晰•AgentState 只回答状态长什么样消息列表、当前步数、最大步数、token 估算•AgentLoop 只负责状态怎么演进调 LLM 之后追加什么、调工具之后追加什么•持久化、记忆、Checkpoint 全部是 AgentState 的扩展点主循环代码一行不用动回到订单案例AgentState 在小明这次对话里装的是messages [ {role: system, content: 你是订单助手可调用 query_order / request_refund}, {role: user, content: 上周买的耳机怎么还没到订单 2026052001...}, {role: assistant, tool_calls: [{name: query_order, args: {order_id: 2026052001}}]}, {role: tool, content: {status: 运输中, eta: 今晚 20:00}}, {role: assistant, content: 你的订单还在路上预计今晚 20 点送达...}]step 2注意messages是按时间顺序累加的——这个顺序就是 Agent 的记忆。把它扔进数据库就是持久化把它压成摘要就是上下文管理所有高阶玩法都站在这个数据结构上。五、LLMClient一个适配器模式这一节直接讲设计模式因为它本质就是教科书答案。问题描述你今天用 OpenAI明天 leader 说咱们要支持私有部署换 vLLM后天合规说国内业务要走 DeepSeek。如果 AgentLoop 里直接openai.chat.completions.create(...)你每次换模型都要改主循环——这是耦合。适配器模式Adapter Pattern所有 LLM 提供者实现同一个接口业务只依赖接口。┌────────────────────────┐ │ interface LLMClient │ │ chat(messages, tools) │ └────────────┬───────────┘ │ ┌──────────────┼──────────────┬──────────────┐ ▼ ▼ ▼ ▼ OpenAIClient ClaudeClient OllamaClient DeepSeekClientAgentLoop 只看到client.chat(...)至于底下是谁、传什么、怎么解析都不关心。回到订单案例客服 Agent 上线时用 GPT-4o三个月后小明所在的省份要求数据不出境公司切到 DeepSeek。如果 LLMClient 抽象做对了改动只在两处注入一个DeepSeekClient 改一行配置。AgentLoop、ToolRegistry、AgentState 全部不动。这个抽象顺手解决三件事都是面试加分点真实痛点抽象后怎么解模型偶尔超时在 LLMClient 外包一层 RetryClient装饰器/Wrapper 模式主模型挂了想兜底加 FallbackClient按优先级顺序尝试不同模型的 Tokenizer 不同让每个 Client 自己负责 token 计数对外只暴露count_tokens()一句话LLMClient 是 Agent 跨模型迁移的唯一防线。这道防线垮了整个架构都得重写。六、ToolExecutor解析、执行、收集三件事ToolRegistry 管工具是什么ToolExecutor 管这一轮要调哪些工具、怎么调、出了问题怎么办。两者经常被合并成一个但分开更清晰——一个是数据一个是行为。ToolExecutor 在订单案例里的一次工作流是这样的LLM 这一轮返回 → tool_calls [ { name: query_order, args: {order_id: 2026052001} } ] │ ▼ ┌─────────────────────┐ │ ToolExecutor │ │ ① 遍历 tool_calls │ │ ② 调 Registry │ │ ③ 异常捕获兜底 │ │ ④ 拼成 tool 消息 │ └─────────┬───────────┘ │ ▼ [ {role: tool, name: query_order, content: ... } ]串行 / 并行 / DAG 三个层级对应业务复杂度•串行一个 for 循环逐个调。最简单最可靠最小 Agent 用这个就够。•并行LLM 一次返回 3 个互不依赖的工具调用比如同时查物流 查积分 查优惠券用 ThreadPoolExecutor 并发延迟立刻减一半。•DAG 调度工具 A 的结果是工具 B 的入参——这已经接近 LangGraph 的领域不属于最小。这是给面试官的一个台阶你说我先做串行并行和 DAG 是后续优化方向比上来就讲拓扑排序更得分——因为前者展示了工程取舍后者只展示了你会背名词。七、AgentLoop把所有人串起来的 while来到最容易被低估的模块。AgentLoop 写出来不到 30 行但它的设计决定了整个 Agent 的可控性。伪代码def run(user_input, state, llm, executor, hooks): state.add_user(user_input) whilenot state.is_done(): hooks.before_llm_call(state) msg llm.chat(state.messages, executor.tools_schema()) state.append(msg) hooks.after_llm_call(state, msg) ifnot msg.tool_calls: return msg.content # 终止条件LLM 不再要工具 hooks.before_tool_exec(state, msg.tool_calls) results executor.execute(msg.tool_calls) state.extend(results) hooks.after_tool_exec(state, results) state.step 1 return state.fallback_response() # 超出最大步数兜底面试现场常被追问的两个问题提前回答Q为什么是 while 循环不是递归或状态机•递归每轮加调用栈最大步数限制下退出不优雅也吃栈空间•状态机分支在编译期固定但 Agent 的分支由 LLM 在运行时决定本质对不上•while 循环结构最简单唯一的坑是死循环而max_iterations是 5 行代码就能加的兜底Q那扩展性怎么办日志、审计、权限要加在哪答案是上面那段伪代码里的hooks。Hook 模式让 AgentLoop 在不增加业务感知的前提下被横切Hook业务里通常用来做before_llm_callToken 预估、限流、日志埋点after_llm_call推理时延监控、成本统计before_tool_exec权限校验关键after_tool_exec结果缓存、PII 脱敏on_complete业务指标上报on_error告警、降级订单案例里before_tool_exec是非常重要的一环小明只能查自己的订单不能用query_order查别人的。这个校验绝对不能写在query_order函数里——那样每个工具都要写一遍。写在 Hook 里所有工具自动受控。八、串起来一次完整的订单查询请求到这里五个模块都讲完了。现在我把它们组装起来跑一遍小明那条消息——这是面试时最能拉分的一段因为大多数候选人都只能讲单个模块。┌────────────────────────────────────────────────────────────────────┐│ 用户上周买的耳机怎么还没到订单 2026052001。如果今天还不到我想退款。│└────────────────────────────────────┬───────────────────────────────┘ │ ┌────────────────────▼────────────────────┐ │ AgentLoop.run(user_input) │ │ ① state.add_user(...) │ └────────────────────┬────────────────────┘ │ ════════════ 第 1 轮 ════════════ │ ┌────────────────────▼────────────────────┐ │ hook.before_llm_call → 记日志 token 计数│ │ LLMClient.chat(messages, tools_schema) │ │ → assistant: tool_calls[query_order] │ └────────────────────┬────────────────────┘ │ ┌────────────────────▼────────────────────┐ │ hook.before_tool_exec │ │ → 校验小明 ∈ 订单 2026052001 的所有者 ✅│ │ ToolExecutor.execute([query_order]) │ │ → 调 ToolRegistry → 真实业务系统 │ │ → result: {status: 运输中, eta: 今晚 20:00}│ └────────────────────┬────────────────────┘ │ ════════════ 第 2 轮 ════════════ │ ┌────────────────────▼────────────────────┐ │ LLMClient.chat(state.messages, ...) │ │ → assistant.content (不再有 tool_call): │ │ 你的订单还在路上预计今晚 20:00 送达。│ │ 如果到 24:00 还未送达可以告诉我 │ │ 我帮你发起退款。 │ └────────────────────┬────────────────────┘ │ ▼ state.is_done() True返回注意几个关键点LLM 在第 1 轮没有同时调request_refund—— 因为它读懂了如果今天还到不了是条件句目前不需要退款。这是 Agent 比 Workflow 强的地方条件判断由 LLM 在运行时做你不需要预先画分支。第 2 轮 LLM 没有再调工具—— 这是 AgentLoop 的终止信号。while 循环的退出条件就是LLM 不再要工具给出最终回答。整条链路上 AgentLoop 完全不知道工具是什么、模型是什么—— 它只串模块。这就是为什么你换模型、加工具、改业务都不用动主循环。把这张图记在脑子里。面试官问你的 Agent 怎么处理 XX 业务——你只需要把这张图抄一遍把工具名换成对应业务的就行。九、上线后会撞墙的三件事最小 Agent 跑通了但生产环境的真正难题在五模块跑通之后才开始。这三件事每一件都对应模块的扩展点9.1 持久化小明明天还能接着聊吗最小实现里 AgentState 只在内存。一旦进程重启就什么都没了。扩展点在AgentState—— 让它能dump()成 JSON下次能load()回来。落地形态可以是• 短会话Redis按 session_id 存• 长记忆MySQL/Mongo按 user_id 存• 检查点每 N 步落一次盘崩溃后从最近检查点恢复底层细节因业务而异但关键是 AgentLoop 一行不改——它只看到一个 AgentState 对象至于这对象是新建的还是从存储里 load 出来的与它无关。9.2 上下文窗口聊到第 30 轮怎么办小明聊到第 30 轮messages 累计 60K token超过模型上下文窗口 → 报错。扩展点同样在AgentState提供两种策略•截断丢最早的非 system 消息简单但会丢信息•压缩把早期对话用 LLM 摘成几百 token 的摘要替换原消息线上常见做法是混合——近 5 轮原文保留再早的全部压缩成一段 summary。这一层逻辑封装在 AgentState 内部主循环每次state.messages拿到的就是已经处理过的版本对 LLMClient 完全透明。9.3 用户自定义 SKILL不动核心代码扩能力最后一个高频面试题“如果产品同学想加一个『售后协商』能力但不让你改 Agent 主代码怎么做”答案是 SKILL 系统。SKILL 不是 Tool差别要说清楚• Tool 是原子操作对应一段代码如query_order• SKILL 是场景 指令 工具集合的能力包不含执行代码——它告诉 LLM 在什么场景下用哪些工具、按什么思路用一份 SKILL 长这样伪 YAMLname: after_sales_negotiationdescription: 处理用户的售后协商包括退换货、补偿沟通instruction: | 当用户表达不满或要求退款时 1. 先用 query_order 确认订单状态 2. 如果订单仍在 7 天内 → 优先推荐换货 3. 如果用户坚持退款 → 调 request_refund并附带 5 元补偿券tools: - query_order - request_refund - issue_coupontrigger_keywords: [退款, 不满意, 投诉, 太慢了]怎么注入 Agent把所有 SKILL 的instruction拼到 system prompt 里就行。AgentLoop 一无所知。LLM 自己读上下文自己决定何时激活哪个 SKILL——这叫隐式路由比硬编码 if-else 优雅得多。用户的扩展流程写工具 → 注册到 ToolRegistry → 写一份 SKILL 描述 → 丢进 SKILL 目录自动加载。全程不碰主循环。这一节展开下去能写一整篇但在最小 Agent 的语境下知道这一点就够了SKILL 是声明式的能力包注入点是 system prompt ToolRegistry 子集筛选。再深入是 agent-05 的题。十、和主流框架的对应关系理解五模块之后再看框架就清晰多了模块你手写的最小实现LangChainLangGraph工具登记ToolRegistrytool ToolExecutorToolNode状态管理AgentStateBaseMessage MemoryState SchemaLLM 调用LLMClient 抽象ChatModel.invoke()同左工具执行ToolExecutorAgentExecutor._executeToolNode.run()主循环AgentLoopwhile max_iterationsAgentExecutor.run()StateGraph 的 Node/Edge我对框架的判断只有一句框架替换的是编排方式不是能力本身。• LangChain 的AgentExecutor内核仍然是一个 while 循环只是被 Chain 包了一层• LangGraph 把 while 换成了图多了 Checkpoint、Interrupt、Human-in-the-loop 等生产级能力知道框架帮你省了什么、没省什么——你才知道什么时候该用、什么时候该绕。先理解最小 Agent再理解框架——反过来很容易学成只会用不会改。十一、白板复盘卡5 分钟能画的图 3 句话如果面试官 5 分钟后让你停笔你要交付的是这张图加这三句┌────────────────── AgentLoopwhile max_iterations─────────────────┐│ ││ AgentState ←───── 状态messages / step / token ││ ││ LLMClient ──→ 接口抽象挂 OpenAI / Claude / 本地 ││ ││ ToolExecutor ──→ 解析 tool_calls 执行 兜异常 ││ └───────→ ToolRegistryname → (fn, schema) ││ ││ Hooks: before_llm / before_tool / on_error ← 权限、日志、监控 ││ │└───────────────────────────────────────────────────────────────────────┘3 句话总结Agent 的本质是反馈驱动循环——LLM 推理、工具执行、状态更新三件事在 while 里反复发生直到 LLM 自己说我不再需要工具了。五模块的拆分原则是能否独立替换——换模型只动 LLMClient加工具只动 ToolRegistry加权限只加 Hook主循环一行不改。框架替换的是编排方式不是能力本身——理解了五模块再看 LangChain/LangGraph你才知道每个 API 在解决什么问题。三个面试加分回答Pre-cached遇到直接抛• “你的 Agent 能换模型吗” →能LLMClient 是抽象接口OpenAI / Claude / 本地模型只是不同适配器。• “工具能并行调用吗” →能ToolExecutor 有串行/并行两种模式没依赖的工具用 ThreadPool 并发有依赖才需要 DAG 调度。• “用户能自己加技能吗” →能SKILL 是声明式能力包instruction 工具子集通过 system prompt 注入Agent 主代码不用改。十二、写在最后我自己在公司带 Agent 项目时踩过最大的坑不是某个具体技术问题而是一开始没把这五块分干净——后来加持久化、加权限、加多模型支持每一次都在改主循环每一次都在埋 bug。所以手写最小 Agent 不是面试技巧是工程纪律。把五块切干净后面所有的复杂需求都是在某个模块上继续生长而不是把已有代码挖开重写。学AI大模型的正确顺序千万不要搞错了2026年AI风口已来各行各业的AI渗透肉眼可见超多公司要么转型做AI相关产品要么高薪挖AI技术人才机遇直接摆在眼前有往AI方向发展或者本身有后端编程基础的朋友直接冲AI大模型应用开发转岗超合适就算暂时不打算转岗了解大模型、RAG、Prompt、Agent这些热门概念能上手做简单项目也绝对是求职加分王给大家整理了超全最新的AI大模型应用开发学习清单和资料手把手帮你快速入门学习路线:✅大模型基础认知—大模型核心原理、发展历程、主流模型GPT、文心一言等特点解析✅核心技术模块—RAG检索增强生成、Prompt工程实战、Agent智能体开发逻辑✅开发基础能力—Python进阶、API接口调用、大模型开发框架LangChain等实操✅应用场景开发—智能问答系统、企业知识库、AIGC内容生成工具、行业定制化大模型应用✅项目落地流程—需求拆解、技术选型、模型调优、测试上线、运维迭代✅面试求职冲刺—岗位JD解析、简历AI项目包装、高频面试题汇总、模拟面经以上6大模块看似清晰好上手实则每个部分都有扎实的核心内容需要吃透我把大模型的学习全流程已经整理好了抓住AI时代风口轻松解锁职业新可能希望大家都能把握机遇实现薪资/职业跃迁这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】