从零搭建最小可用Agent:核心原理与Python实战指南

📅 发布时间:2026/10/17 3:42:03
从零搭建最小可用Agent:核心原理与Python实战指南
我见过不少程序员第一次接触Agent时的反应这不就是个套了提示词外壳的聊天机器人等他们自己动手写完一个才发现真正的Agent是一套让大模型“自己决定下一步干什么”的运行循环而不是写一段固定流程的脚本。这篇文章就把我跑通一个最小可用Agent的完整路径摊开讲从本质原理、选型思路到核心代码、真实踩坑尽量做到保姆级适合已经会用Python、想在业务里落地Agent但还没系统性搭过的读者。先说清Agent能做什么它不是回答问题的ChatBot而是能调用工具、读取记忆、根据结果反复决策的自动化程序。举个简单例子你让它“查一下这批货什么时候能发如果延期超过一天就自动发一封催办邮件”它要知道先查库存、再查物流、判断条件、最后触发发信动作每一步都是模型现场决定而不是写死的。这篇文章不会只给概念我把我实现一个mini-agent的完整过程、代码、调试思路、上线前要过的关卡都写出来你可以直接照着复现也可以拿其中的思路改成自己的版本。1. Agent的本质你以为在写程序其实在搭一套“会做决定的循环”1.1 Agent不是ChatBot核心区别在于“行动”闲聊机器人是“你说一句它答一句”所有信息都在模型参数和输入上下文里模型不会主动去查数据库、不会真去调用接口、不会改变任何系统状态。Agent则多了一个关键环节模型可以发起一个“工具请求”程序收到后实际执行外部操作再把结果交还给模型。这个“发起请求→真实执行→返回结果→再次决策”的闭环就是Agent和ChatBot的分水岭。我用一个生活类比帮刚入门的朋友理解Agent像是一个会主动查手册的新实习生。你交代一件事他先去翻资料、查系统、操作软件做完一步之后回来告诉你“我查到A情况要不要按B方案继续”你点头他再动下一步。关键不在于他每一步做得多好而在于每一步做完之后他都能基于真实结果重新决定接下来做什么。这份“决定权”属于运行循环不属于预先写死的那几个if else。1.2 核心循环感知→决策→行动→再感知一个最基础的Agent循环长这样接收用户任务把它作为模型输入。模型分析当前情况和可用工具做出决策。若模型判断需要调用工具输出结构化的“工具调用”标记。程序解析标记执行对应函数拿到真实返回结果。把工具结果以消息形式追加进上下文再次交给模型。循环直到模型输出最终答案或达到最大轮次上限。这个循环看起来简单但真正落地时每一环都有坑。比如第3步模型输出的格式稍微不规范解析就直接崩第4步工具执行失败模型看不到失败原因就开始胡编第6步不设上限一个异常任务能把你的Token消耗拉到天价。后面我会把这些坑一个个展开。1.3 为什么必须靠“函数调用”而不是让大模型直接执行代码很多人会问既然Agent这么聪明能不能让模型直接“操作”我们的系统省掉中间层不能。原因是模型本身不具备执行能力它只是一个概率函数输入文本预测下一段文本。所谓的“调用工具”在模型眼里就是输出一段特定格式的JSON类似{name: query_stock, arguments: {item_id: SKU001}}。真正去执行这个查询的是我们程序里的代码。所以开发Agent本质上是在做两件事一是把业务能力包装成模型能理解的“工具清单”二是写一个解释器去执行模型的决定并反馈结果。模型负责“判断用什么工具、填什么参数”程序负责“替它把手伸出去”。这个分工决定了你要花很大力气在工具定义质量上而不是指望模型自己会干活。1.4 不是所有需求都该上Agent我也见过反面案例明明是一个固定流程的订单状态查询有人非要做成Agent结果模型偶尔把查询参数填错线上事故频发。判断要不要用Agent看三个特征任务是否有多个分支、是否依赖外部动态数据、是否需要在执行过程中根据结果做调整。如果只是“接到请求→查固定字段→返回固定模板”普通的工作流引擎更合适。Agent的价值场景是“无法提前穷尽所有情况的自动化”。2. 动手前的选择题语言、模型和框架到底怎么选2.1 编程语言Python起步最顺但别把状态写进进程内存首选Python理由很实在处理JSON方便、发HTTP请求生态成熟、文件和数据加工库多而且多数大模型SDK都优先支持Python。你不需要引入特别复杂的依赖标准库加一个requests就能把核心循环跑起来。但有一个容易被忽略的点Agent运行循环是长时延、多轮次的不适合直接塞在Web业务线程里同步等结果。更合适的做法是把Agent封装成独立服务通过消息队列或异步任务接口接收请求执行完再回传结果。如果是团队里已经有Java/Go的主服务也不必推翻重来用Python单独起一个worker当“决策引擎”即可。2.2 大模型接口选型不是越强越好工具调用稳定性优先选模型时大多数人第一眼看“谁聪明”但做Agent最核心的是“工具调用格式稳定性”——也就是模型能不能每次都规规矩矩输出可解析的JSON。再聪明的模型如果十次里有三次把自然语言和函数标记混在一起你的解析器就会一直报错、Agent根本跑不稳。我一般按下面几个维度做选择对比选型方向主要优点主要代价适合场景商用大模型API工具调用稳定、开箱即用、省运维数据出域、单次成本高快速验证、非敏感业务私有化部署的开源模型数据全在内网、按量成本低需要卡和运维、效果可能不稳数据敏感、高并发自研小模型极低延迟、完全可控需要训练数据、效果上限有限场景极简单、单工具调用实际建议是第一版直接选一个工具调用能力强的商用API把你全部的注意力放在打磨工具和闭环逻辑上。等跑通了、看清瓶颈了再考虑往私有化迁移。一开始就自己训模型大概率会在模型效果上耗掉大量时间。2.3 从零写还是用框架我建议第一个Agent从零写现在开源的Agent框架很多自带记忆模块、工具调度、可视化面板听起来很方便。但问题在于抽象层太厚一旦出错你很难判断是模型问题、工具问题还是框架调度问题。我的经验是第一个Agent一定从零手写别再问为什么。从零写你能搞懂每一行代码在做什么尤其是工具调用JSON的解析、消息历史的组织、错误反馈的手法。这些基础不牢后面用框架也是黑盒踩坑。等你完整跑通过一个mini-agent再用框架就属于“知其然也知其所以然”框架帮你省掉的只是重复劳动而不是替你解决理解问题。3. 最小可用Agent的完整实现从识别意图到动手干活3.1 先画任务边界第一版只做三个工具我拿一个常见场景举例企业内部的商品库存查询助手需要对接库存接口、价格接口和预留申请接口。第一版不要贪多先只做三个工具函数名参数返回内容query_stockitem_id: string日期: string该商品在指定日期的可用库存数和预计补货时间query_priceitem_id: string商品单价、所属分类submit_reservationitem_id: stringquantity: intremark: string预留成功后生成的单号或失败原因工具定义写得好不好直接决定模型会不会用。每个参数名要用业务人员会说的词别用内部表字段返回字段要稳定枚举值要在描述里写清楚。比如quantity要注明“正整数超过可用库存会失败”模型就知道提交前先查库存。3.2 核心循环代码一个不依赖框架的mini-agent下面这段是我实际跑通过的简化版伪代码用Python写刻意不依赖任何大模型SDK把“模型调用”抽象成一个llm.chat函数实际项目里那一层就是某个大模型API的封装。import json def run_agent(user_input, tools, max_rounds8): messages [ {role: system, content: 你是企业库存助手。先查询再判断最后行动。不要编造数据。}, {role: user, content: user_input} ] for round_index in range(max_rounds): response llm.chat(messagesmessages, toolstools) if response.get(finish_reason) stop: return response[answer] if response.get(tool_calls): for call in response[tool_calls]: tool_result execute_tool(call[name], call[arguments]) messages.append({ role: tool, tool_call_id: call[id], content: tool_result }) # 这一轮工具结果全部追加后继续循环让模型根据结果做下一轮决策 continue if response.get(answer): return response[answer] return 已达到最大轮次任务终止请人工介入execute_tool是真正执行函数的地方我建议统一加try/except连续失败也要把错误用JSON返回给模型而不是让流程静默断掉def execute_tool(name, arguments): try: handler TOOL_HANDLERS[name] result handler(**arguments) return json.dumps({ok: True, result: result}, ensure_asciiFalse) except Exception as e: return json.dumps({ok: False, error: str(e)}, ensure_asciiFalse)这段代码虽然短但它包含了Agent的完整灵魂模型提申请程序真执行结果回喂给模型循环往复直到能给出最终答案。max_rounds是防死循环的第一道保险我一般默认8轮内部工具多的任务可以调到16轮但不要更高轮次一旦异常Token消耗会非常恐怖。3.3 工具返回要“结构化失败”别让模型自己脑补很多新手会犯一个错工具执行出错直接在代码里抛异常导致模型拿到的上下文里没有这个错误信息它就会基于自己的“直觉”补一个看似合理的答案。比如库存接口超时工具返回空模型就告诉你“库存不足”这是完全错误的推断。所以工具返回的内容必须包含状态字段成功返回{ok: true, result: {...}}失败返回{ok: false, error: 库存服务超时}。模型看到“查询失败”才会如实反馈而不是替你做决定。这一点在Agent开发里怎么强调都不过分。3.4 记忆机制短期靠上下文长期靠摘要和存储Agent的记忆分两层。短期记忆就是上面的messages数组模型靠它“记得”前面说过的话。但大模型上下文窗口有限对话一久历史消息就会撑爆或稀释重点。我的做法是设置一个阈值比如超过20条消息后把前面的对话扔给模型做一次摘要浓缩成一段“进展摘要”替换掉旧消息再塞回system里。相当于给Agent做了一个“工作日志”。长期记忆则用来存需要跨会话保留的信息用户偏好、历史订单、审批状态。第一版不需要上向量库直接写进JSON文件或数据库就能满足大部分业务。等数据量大了、检索方式复杂了再考虑向量化检索。记忆是Agent的第二个上限短期记忆管“当前任务能不能连贯”长期记忆管“这个Agent是不是越用越懂你”。4. 我踩过的六个坑和对应的排查思路4.1 模型把函数调用和普通回答混在一起现象response[tool_calls]没有值但模型输出了一段解释后面又跟了一个残缺的JSON。排查时我先打印了模型原始响应发现模型在“自言自语”解释为什么要查库存之后才抛出工具标记。根因是提示词给得太啰嗦模型拿捏不准该直接答还是该调用。解决方法是系统提示里明确写“如果你需要工具数据只输出工具调用标记不要输出解释文字”同时优先选择支持“强制结构化输出”的模型接口。现在很多模型都支持response format限定能大幅减少这种解析灾难。验证方法连续跑10个需要查库存的用例看tool_calls的解析成功率是否达到100%。如果还有少数混着自然语言的就在解析失败时补一层“重试一次要求模型重新只输出JSON”的逻辑。4.2 工具返回结果太长把上下文直接撑爆现象查询一个商品的可用库存后端接口返回了整整一百行的库房明细Agent还没开始判断Token就先烧掉一大截有时还直接超过模型上下文上限。根因没有对工具返回值做裁剪系统给什么模型就吃什么。解决方式有二一是接口层就只返回模型判断需要的核心字段比如总库存、最近补货日期二是对大批量数据做截断只返回前N条并明确告诉模型“结果已截断共100条当前显示前10条”。千万别把所有数据都塞进去。4.3 Agent陷入循环反复调用同一个工具现象日志里query_stock被调用了十几遍参数一模一样结果也一样但Agent还在继续问。根因模型没有同一条工具结果去重机制它每轮都觉得自己“刚拿到新数据”尤其当工具结果里没有时间戳时。排查时我在每个tool result前面加了query_time字段循环次数仍然没降下来最终是靠代码层做去重如果同一轮次内出现相同函数名和相同参数直接返回上次的结果不再重复执行。同时给max_rounds设了更小的值。如果还循环不止就要考虑是不是任务本身缺信息引导模型转去问用户。4.4 工具执行失败后模型开始胡编结果现象查询接口超时工具返回了空结果Agent一本正经地告诉用户“该商品库存为0建议补货”。这其实比报错还糟糕因为用户会把这个假结果当真。根因工具失败时的content是空字符串模型把它当成了合法结果并做了推断。我在所有工具出口统一包了状态字段错误时明确告诉模型“查询失败无法获得真实数据”并且系统提示加了一句“没有真实工具结果时禁止回答任何业务数据”。这之后还在代码里加了校验如果最终回答里的数据点无法追溯到任一成功工具响应就打回重答。4.5 并发使用时状态互相污染现象同一个Agent实例同时被两个会话使用A用户的消息跑到了B用户的上下文里回答张冠李戴。根因我用了一个全局的messages数组所有会话都往里面追加消息并发时互相覆盖。排查时要做的第一件事就是梳理所有全局可变状态。正确做法是每个会话独立维护一份messages上下文按session_id隔离工具执行状态的缓存也要带上session_id前缀。之后我做了并发压测同时开50个会话每个会话发不同商品ID的查询验证回答和上下文完全隔离。4.6 异步工具调用时结果顺序错乱现象Agent同时发起两个工具调用设计上是“先查库存再查价格”但因为异步执行价格结果先返回并按顺序拼进上下文模型拿到后发现工具返回顺序与发起顺序不一致产生幻觉。根因靠“列表顺序”对应“工具结果顺序”是脆弱的。正确做法是给每个tool call都生成唯一ID工具执行完的结果必须带上这个ID按ID归集到对应的调用位置全部完成后再统一交给模型。这个模式和并发安全是同一套理念所有状态都按ID关联不要依赖线程执行顺序或JSON数组索引。5. 从“跑通”到“敢上线”进阶实操5.1 先别搞“多Agent群聊”用主从分工就够了很多人在网上看到多Agent协作的演示很酷回去就照抄结果一堆Agent互相等、互相绕问题没解还多花好几倍Token。我在实际项目中用的最顺的其实是简单的主从模式一个主Agent负责理解用户意图、拆解任务把子任务下发给几个专门的子Agent每个子Agent返回结构化结果最后由主Agent汇总输出。比如一个数据分析助手主Agent接到“分析本月销售额并生成图表”就先拆成“查数据”和“画图表”两个子任务分别交给不同子Agent而不是让同一个Agent在一长串消息里反复横跳。这种结构对调试、日志追踪都友好每个子Agent只用干一件小而清楚的事。5.2 没有日志就别上线每个行动都要可追溯Agent上线前最重要的一件事是给每一次模型决策和工具调用打日志。我的日志模板是固定字段request_id贯穿整个会话、当前轮次、模型输入消息条数、模型决策内容是直接回答还是调用工具、工具名、参数、执行耗时、返回结果摘要、Token消耗。每天定时跑一遍日志分析能很清楚地看到每个Agent卡在哪一步、哪些工具调用失败率最高。没有日志的Agent跟没有监控的下线系统一样危险。模型的行为是概率性的你今天测过的用例明天都可能翻车日志就是事后追查的唯一线索。5.3 上线前我必过的三道关确认、超时、降级第一道关是人机确认节点。凡是影响现实业务的操作比如提交订单、发送邮件、修改数据我都在流程里加了一个“待人工确认”状态Agent执行到这里会把建议动作展示给用户等确认后再执行。人在回路不是倒退是对概率模型上的一道安全锁。第二道关是每个工具访问必须有超时与熔断。外部接口超时设定不能超过5秒连续失败三次直接熔断不让Agent在死循环里重试到天荒地老。第三道关是降级通路。Agent异常退出时用户不能面对一个白屏而是收到一句“当前服务繁忙请稍后再试或联系人工客服”。降级文案是系统能力的一部分不是可有可无的补丁。5.4 成本优化缓存、路由、摘要三板斧Agent烧钱的速度我相信做过的人都懂尤其长对话和多轮工具调用。我做降本的三个抓手第一是缓存。凡是工具参数完全相同的调用短时间窗口内直接复用结果避免模型反复触发同一查询。第二是路由。简单任务走小模型复杂任务才走大模型这个判断可以在入口处人工标记也可以让一个小模型先做意图分级。第三是对话摘要。长聊天的历史记录定期压缩成摘要别永远带着完整上下文。这三板斧能让同样业务量的成本降到原来的三四成效果很直观。我个人现在开发新Agent的标准流程是先手写最小闭环跑通再加记忆再上日志和监控最后才考虑是不是引入别人封装好的框架。你上手时千万别急着把“自动化”拉到最满哪怕技术上已经成熟也建议每个关键决策保留人工确认链路。Agent的自主性越高你越要清楚它的边界在哪里。