资讯动态

手写AI Agent全流程拆解:从零掌握核心闭环与工程化

发布时间:2026/10/13 4:05:18 来源:尧图企业网站定制
1. 先别急着手写你缺的不是框架是拆解能力1.1 一次让我破防的线上问题去年我做内部支撑的时候有位同事用某款热门的Agent框架搭了一个客服助手跑了两周就开始出幺蛾子。表现很诡异同样的问题上午答得挺好下午就开始胡说八道后台一看token消耗曲线直接起飞。我们一群人围着框架源码翻到半夜最后发现原因是循环退出条件没配好——模型一直在调用工具→拿到结果→再调用工具的圈里空转压根没走到给用户最终回答这一步。那一刻我最大的感受不是框架有bug而是如果当时有人能把这个循环从头到尾手写一遍这种问题五分钟就能定位。框架帮你封装了大量细节同时也把调试线索一并吞掉了。你以为是你在用Agent实际是Agent在替你做决定你只是在填配置。这也是今天这篇内容的起点。标题里那句从调包侠到造轮子的人说的就是同一个事儿别再满足于框架调通了、Demo能跑了,而是去弄明白一个AI Agent系统到底是由什么组成的然后亲手把它搭出来。这套东西不需要你懂深度学习、不需要你推导Transformer公式只要你会写点Python、知道HTTP请求大概怎么回事就能跟着做出来。1.2 手写Agent写的到底是什么先说清楚一个容易误解的点手写Agent不是让你从零训练一个大模型也不是让你自己实现注意力机制。绝大多数场景下你写的是一套调度逻辑——怎么把用户的请求送给模型怎么解析模型返回的内容怎么决定要不要调用某个工具怎么把工具的结果交还给模型让它继续思考。用做饭类比。用框架开发Agent相当于买速冻料理包微波炉叮三分钟就能上桌味道稳定但你没参与任何环节。手写Agent则是自己去菜市场买菜、切菜、开火每一步都清楚放了多少盐。一开始肯定比速冻慢但食材一变、口味要调的时候你的主动权完全不同。所以造轮子的人这个说法其实有点夸张我们造的不是汽车发动机那种级别的轮子而是把Agent这条流水线上的传送带自己装一遍。装过之后你再看那些开源框架就不再是黑盒子使用说明书而是一行行可以和你自己的代码对照的参考答案。1.3 零基础到底需要什么基础课程标题写了零基础我必须把这个边界划清楚免得有人被劝退也免得有人带着错误预期进来。我理解的零基础是指零AI基础但不是零编程基础。你需要具备的最低能力大概是这三样Python基础语法会写函数、会处理列表和字典、知道什么是装饰器就更好了。JSON的基本概念能看懂一段JSON的结构能用Python的json库做序列化和反序列化。最朴素的HTTP知识知道POST请求是干什么的能在代码里调用一个接口。如果你这三样都没问题那剩下的交给课程就行。整个路线的设计逻辑是每节课只增加一个新零件第一节课搭一个只会对话的极简助手第二节课给它加上工具调用第三节课处理多轮记忆第四节课做任务规划第五节课做工程化加固。每加一块你都会清楚地知道原来框架帮我处理的是这个东西。我见过不少非科班出身的朋友从零开始跟我这个路线走下来最后不仅能独立写出一个可用的小Agent还能看懂主流框架的核心源码。这就是调包侠到造轮子的人的真实路径不是靠天赋是靠把每一层都亲手拆开再装上。2. 解剖一个最小可用Agent五个零件与一个闭环2.1 Agent Loop所有框架都在兜售的同一条流水线如果你把市面上任何Agent框架扒开看会发现它们内核几乎都是同一个循环模式只是换着花样包装接收任务→丢给大模型→模型说我要调用某个工具→执行工具→把结果喂回去→模型继续推理→直到模型给出最终答案。这个循环在业界一般叫思考-行动-观察模式。思考是模型分析当前情况行动是决定调用哪个工具或直接回答观察是拿到工具结果后重新评估。我写的时候喜欢叫它Agent闭环因为本质上这就是一个带反馈的流水线和工厂里的质量检测循环没什么区别。理解这个闭环是你从会调框架到会写Agent的分水岭。很多时候你觉得框架难用是因为它的流水线里夹带了太多你不需要的环节——什么记忆模块、向量数据库、多智能体通信全都默认装上。而你自己搭的时候完全可以只保留核心闭环按需添加。这种减法的自由是手写最大的红利。2.2 模型调用层你只需要一个薄薄的封装闭环里的第一步是向模型发起请求。很多人在这一步就被框架带偏了以为调用大模型是多高级的事其实核心就一句话发一个HTTP请求把对话历史和工具定义塞进去然后取回模型返回的消息。关键点在于你传给模型的对话历史是一串消息列表每条消息有角色和内容。系统消息相当于给模型设定人设和规则用户消息是输入助手消息是模型之前的回复工具消息是工具执行结果。整个记忆本质上就是这张不断变长的消息表。我在课程里反复强调一件事先把这个最薄的调用层写好别加任何花活。不要想着一次封装就支持流式输出、多模型切换、自动重试——那些都是后话。第一版只要能同步调用、能拿到完整JSON响应就行。越薄越好因为它只要做好一件事后续所有复杂逻辑才可以建立在这块稳定地基上。2.3 工具层让Agent能动手的关键协议Agent和普通聊天机器人最大的区别就在工具层。所谓工具,就是一组你预先定义好的函数——查天气、算算术、查数据库、调某个内部接口都可以。但要让模型知道什么情况下该用哪个工具、参数怎么填你必须把每个工具翻译成一份结构化的说明书也就是JSON Schema。这份说明书一般包括工具名、功能描述、参数列表以及哪些参数是必填的。模型读了这份说明书后如果觉得需要调用工具就会在回复里夹带一个特殊的工具调用指令告诉你它想调用哪个函数、参数填了什么。你的代码需要解析这个指令执行对应函数再把结果打包成一条工具消息送回给模型。我最初手写工具层的时候犯过一个经典错误只给工具写了名字和参数没写仔细的功能描述。结果模型经常在需要调用工具的时候不调或者在一个工具上死磕。后来才明白这部分说明书不只是给机器看的协议更是在说服模型做出正确决策的提示词——描述写得越清楚模型越容易选对。2.4 记忆层上下文管理的边界问题很多人一提到Agent的记忆第一反应就是上向量数据库。这个想法不能说错但放在手写入门阶段就是个陷阱。向量数据库解决的是长期记忆的检索问题而Agent日常运行的短期工作记忆,就是那条越来越长的消息列表。真正麻烦的问题在于消息列表不能无限变长。模型有上下文窗口限制而且即便没到硬上限越长的上下文也意味着更慢的响应和更高的成本。所以记忆层的核心不是怎么存更多而是怎么在有限空间里保留最重要的信息。最朴素的方案就三招截断、摘要、滑动窗口。具体策略我放到第四章细讲这里先记住结论不要迷信所谓记忆框架大部分业务场景一个截断策略加一段摘要提示词就能解决。2.5 停止条件别让Agent空转到破产新手写第一个Agent循环时最常见的bug就是循环停不下来。模型没说结束你的代码就一直调用、一直喂结果最后要么超时要么token烧穿。所以我一直把停止条件当作和主循环同等重要的零件来教。终止循环的条件就三个模型回复里没有工具调用指令说明它决定直接回答问题达到你设定的最大步数上限强制结束某次工具调用或解析出了问题触发兜底逻辑返回提示。这三个条件缺一不可。第一个是正常出口第二个是安全阀第三个是异常保护。把这三个条件写全了你的Agent才算是能放出去跑的东西而不是实验室里的玩具。3. 手写一个能跑的Agent代码逐段拆解3.1 先搭一个不依赖框架的LLM调用层下面这段代码我会直接用一个通用Chat Completions风格接口来写你换成自己实际使用的模型服务地址和密钥就行。它做的事非常朴素接收消息列表和工具定义返回模型的最新回复。import json import requests def call_llm(messages, toolsNone, modelyour-model): url https://your-llm-endpoint/v1/chat/completions headers { Authorization: Bearer your-api-key, Content-Type: application/json, } payload { model: model, messages: messages, temperature: 0.3, } if tools: payload[tools] tools resp requests.post(url, jsonpayload, headersheaders, timeout60) resp.raise_for_status() return resp.json()[choices][0][message]这里有两个参数我要多解释一句。第一是temperature设成0.3是为了让输出更稳定Agent在调用工具时必须按照固定格式输出太高的随机性会导致它频繁写错参数或编造工具名。如果你做的是创意写作类任务temperature才需要往高调但做工具调度时低温度是默认项。第二是timeout设成60秒防止模型服务端迟迟不返回把主流程卡死。第一版不用支持流式输出。流式是性能优化不是功能必需等你的核心闭环跑通了再回来加也不迟。3.2 工具注册把函数翻译成模型能懂的JSON我习惯用装饰器来做工具注册这样每新增一个工具只需要写一个普通函数加一段装饰器不需要去改主循环里的任何代码。这算是一个很小的架构设计但能让你后期加工具时非常舒服。TOOL_REGISTRY [] def tool(name, description, parameters): def decorator(fn): TOOL_REGISTRY.append({ definition: { type: function, function: { name: name, description: description, parameters: parameters, }, }, fn: fn, }) return fn return decorator tool( get_weather, 根据城市名查询天气适合回答与天气相关的问题, { type: object, properties: { city: {type: string, description: 城市名例如北京、上海} }, required: [city], }, ) def get_weather(city: str) - str: return f{city}天气晴24℃注意看这段代码的层次TOOL_REGISTRY里每一项都包含两部分definition是给模型看的说明书fn是真正要执行的Python函数。这样做的好处是主循环在组装请求时只需要把所有的definition提取出来执行时只需要按名字找到对应的fn。模型和你的代码通过工具名参数JSON这套协议沟通彼此都不需要知道对方的内部实现。写工具描述时有个实用技巧description不要只写查天气要写清楚什么场景下用、有什么副作用。比如你有一个发送邮件的工具描述里最好注明会给真实收件人发送邮件仅在用户明确要求时调用。这类描述能显著降低模型乱调工具的概率。3.3 主循环思考、决策、行动、观察现在写最关键的主循环。逻辑不复杂把用户问题放进消息列表反复调用模型只要有工具调用指令就执行并把结果追加回去直到模型给出纯文本答案。def dispatch_tool(name, args): for item in TOOL_REGISTRY: if item[definition][function][name] name: try: return item[fn](**args) except Exception as e: return f工具执行出错: {e} return f未找到工具: {name} def run_agent(query, max_steps5): messages [ {role: system, content: 你是智能助手。你可以调用工具获取信息最终必须用自然语言回答用户。}, {role: user, content: query}, ] tool_defs [t[definition] for t in TOOL_REGISTRY] for step in range(max_steps): assistant_msg call_llm(messages, toolstool_defs) messages.append({ role: assistant, content: assistant_msg.get(content) or , tool_calls: assistant_msg.get(tool_calls), }) # 没有工具调用说明模型决定直接回答循环正常结束 if not assistant_msg.get(tool_calls): return assistant_msg.get(content) or # 逐个执行工具调用并把结果作为工具消息追加进对话 for call in assistant_msg[tool_calls]: fn_name call[function][name] args json.loads(call[function][arguments] or {}) result dispatch_tool(fn_name, args) messages.append({ role: tool, tool_call_id: call[id], content: str(result), }) # 达到最大步数强制停止 return 已达最大轮数停止执行。请调整问题或检查工具调用。这段代码里有两个细节容易被新手忽略。第一assistant消息里即使没有content也要把tool_calls原样保存并回传给模型这是工具调用协议的一部分漏了会导致下一次请求的上下文不完整。第二tool_call_id必须一一对应工具消息要关联到具体的那次调用模型才能正确理解这个结果是对应哪次调用。3.4 实际跑起来的效果上面这个Agent虽然只有几十行代码但已经具备完整的闭环能力。用户问上海今天适合出门吗模型会先调用get_weather工具拿到天气结果再根据结果组织回答。整个过程你可以清楚地看到每一步消息是怎么流转的。我建议你第一次跑通时把messages列表打印出来看一遍。你会看到模型先产生一个含工具调用的assistant消息然后是一条tool消息最后又是一条assistant消息给出最终结论。这个三次对话的形态就是Agent最基础的工作方式理解了它后面什么多步规划、多工具协作都是在这个骨架上加东西。4. 从能跑到能打四个绕不开的工程化问题4.1 Token成本失控对话越长烧钱越快手写Agent跑通Demo之后我遇到的第一个真实问题是一个稍微复杂的任务模型来回调用五六次工具一轮对话消耗的token比想象中高得多。如果是生产环境还有大量用户这账单会很感人。解决思路分为两层。第一层是减少无效上下文系统提示词只保留必要的角色设定和规则别把冗长的知识库塞进去每次追加的历史消息可以只保留最近几轮完整对话更早的做摘要压缩。之前提到的滑动窗口本质上就是设定一个消息条数或token阈值超过就把最早的非系统消息丢出去。第二层是摘要压缩——我实战里最常用的方案。当对话接近窗口上限时先把当前核心信息用模型Summarize成一小段文字然后用这段摘要代替之前的完整历史。它的实现不复杂核心逻辑是这样的def compress_if_needed(messages, max_blocks10): # 只压缩中间的历史对话保留系统消息和最近两轮 if len(messages) max_blocks: return messages system [m for m in messages if m[role] system] recent messages[-(max_blocks - 2):] history [m for m in messages if m[role] ! system and m not in recent] summary call_llm([ {role: system, content: 把下面的对话压缩成要点保留关键事实和未完成的任务。}, {role: user, content: json.dumps(history, ensure_asciiFalse)}, ])[content] return system [ {role: assistant, content: f历史对话摘要{summary}}, ] recent这个方案的关键是保留最近两轮完整记录因为最近的信息往往最相关摘要里的信息虽然准确但缺少细节模型追问时可能不够用。压缩不是万能的但作为第一道防线完全够用。4.2 模型输出不规范解析失败不是小概率事件理论上模型应该严格按工具调用协议输出JSON。但实际跑起来你会发现它偶尔会犯各种怪毛病在JSON前后加一段解释文字、把布尔值写成字符串、参数名多一个下划线。这些情况在框架里被静默处理掉了所以你没见过手写之后你就要自己面对这些天才输出。我的处理经验是解析失败时不要直接崩溃先做一次抢救再把问题反馈给模型让它自己改。抢救的逻辑很简单——找到字符串里第一个{到最后一个}之间的内容尝试JSON解析def try_parse_args(raw): if not raw: return {} start raw.find({) end raw.rfind(}) if start -1 or end -1: raise ValueError(f无法从内容中找到JSON: {raw}) return json.loads(raw[start:end 1])如果抢救失败就构造一条新的系统提示或用户消息明确告诉模型你上次返回的工具参数格式无法解析请重新输出合法的JSON。让模型自己修正通常比你在代码里穷举各种错误情况要省事得多。这也是Agent式开发的一大特点把异常处理的一部分外包给模型。4.3 工具执行出错让Agent学会自救而不是崩溃你自己的代码调用工具函数异常可能来自各种地方参数类型不对、外部接口超时、依赖服务返回了错误码。如果工具一报错就让整个Agent崩溃体验会非常糟糕。更合理的做法是把异常信息当作工具执行结果返回给模型让模型知道这个工具没成功原因是什么然后由它决定是换个参数重试、换一个工具还是直接告诉用户遇到了问题。我在dispatch_tool里已经写了try/except异常信息会拼成工具执行出错: xxx作为工具消息内容。你别小看这一步它带来的行为差异非常大模型看到错误信息后通常会主动纠正参数再试一次或者向用户解释现状。这比直接中断流程或者傻傻地原样重试三次好得多。当然自救也要有边界不然会变成死循环。Max步数上限就是兜底。我见过模型在一个查询类工具上反复用相同参数重试三次的场景如果没有步数限制每次重试都是一笔额外开销。所以错误反馈给模型和强制终止这两层必须同时存在。4.4 没有日志的Agent等于没有黑匣子框架自带日志通常很全但层级太多关键时刻反而不容易找到需要的信息。手写Agent最大的红利就是你自定义日志极其方便。我的做法是在主循环的每一步都落一条JSONL日志记录时间戳、当前步数、模型返回的内容、调用了哪些工具、累计估算消耗了多少token。import time def log_step(step, assistant_msg, tool_resultsNone): entry { ts: time.time(), step: step, content: assistant_msg.get(content), tool_calls: assistant_msg.get(tool_calls), tool_results: tool_results, } with open(agent_trace.jsonl, a, encodingutf-8) as f: f.write(json.dumps(entry, ensure_asciiFalse) \n)事后排查问题时这条日志就是黑匣子。模型哪一步开始胡说八道、哪一次工具调用返回了异常数据、哪一轮循环没有退出翻一下JSONL全都对得上。我甚至建议你把每次请求的完整消息列表也存一份至少存成压缩格式这样可以离线复盘模型的行为模式比在线上瞎猜高效得多。5. 从调包侠到造轮子的人学习路线与心态建议5.1 手写和框架的边界在哪里讲了这么多手写的好处我也得说句公道话不是所有场景都该手写。生产环境里如果团队要快速验证一个复杂的多智能体协作系统成熟的框架确实能帮你省下大量时间。框架的价值在于标准且高频的组件复用手写的价值在于对每一层逻辑拥有掌控力。两者不是对立的而是互补的。我给自己定的选择标准很简单学习阶段、调试阶段、以及对延迟和成本有特殊要求的场景优先手写业务规模大、需要多人协作维护、而且用框架的默认能力就能覆盖需求的时候果断用框架。关键是你得先手写过一遍才有资格做这个判断。没拆过轮子的人是分不清框架不行和我不会用的。5.2 课程路线为什么这样设计很多课程喜欢一开始就讲Agent的宏大架构、各种概念、各类框架横向对比学生听完感觉懂了一动手还是懵。我这个课程完全反着来核心原则就一条每一节课都产出一个能运行的东西每个新增概念都绑定一个必须亲手实现的功能。整个路线分五层第一层做出一个能对话的最小闭环第二层加上工具注册与调用让Agent真正动手第三层解决多轮记忆与上下文压缩第四层加入任务规划让Agent能拆解复杂问题第五层做工程化加固包括日志、错误处理、质量评估。学完前两层你已经能应付大部分常见场景学完第五层你就能自信地说自己造过一个完整的轮子。5.3 我的最后建议如果你现在正准备学这个方向我只有一条建议别囤课别只看不动。找一个周末把这篇文章里的代码从头到尾敲一遍改成你自己的工具和提示词让它跑起来。第一次跑通的那一刻你会对Agent这个词产生完全不同的理解——它不再是什么高深莫测的技术而是一个你亲手排好的流水线。我也得提醒你手写第一版一定很粗糙会碰到各种莫名其妙的解析错误和循环问题。这很正常我写第一版的时候也被模型气到过。但正是这些气人的瞬间让后来用框架时遇到的报错变得亲切起来——因为你知道它背后发生了什么知道该去看哪一环。如果你能坚持到看框架源码像看自己写的代码那一天恭喜你调包侠毕业了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑