
最近有好几个做后端的朋友跑过来问我同一个问题大模型API我早就接上了对话也调得贼溜但想让AI真正帮我“干点活”——查个数据、填个单子、整理个报表——它怎么就跟听不懂一样其实问题不在这一代大模型不够聪明而是大家手里拿着的还只是一个“聊天机器人”并没有把它升级成“AI Agent”。这两者的差距不是多写几行Prompt就能拉平的而是整套架构设计都要换思路。这篇内容是我从零开始开发一个生产级AI Agent的完整复盘按顺序拆解了概念、选型、最小实现、工具调用、记忆设计、多Agent协作和上线前的各种坑。适合有Python基础、调过大模型API但对Agent还没有形成完整认知的人也适合那种已经用LangChain写过Demo、却总是跑不通或者不敢上生产的同学。看完之后你应该能自己搭出一个能真正完成任务的智能体而不是另一个只会“聊天”的玩具。1. 先分清你在做聊天机器人还是在做Agent1.1 三个最常见的Agent误解第一个误解觉得“接了大模型API就等于做了Agent”。远不是一回事。ChatBot是用户问一句、模型答一句答完就结束了。Agent是用户给一个目标模型自己拆解步骤、调用工具、观察结果、修正计划直到拿到最终结果才停下来。一个是问答一个是干活。第二个误解觉得Agent必须很复杂要上LangChain、要上向量库、要搞多智能体。实际上一个最小可用的Agent核心就是一个循环思考、行动、观察、再思考。工具可以只有一个记忆可以先不做这些都不妨碍它被叫做Agent。第三个误解觉得Agent能自己“动手”执行一切。真实情况是模型本身不会执行任何东西它只会输出“我想调用哪个函数、参数是什么”真正去查数据库、调接口、读文件的是你的代码。模型是调度员不是执行员。这个认知很重要后面所有设计都建立在这上面。1.2 Agent的四个基本零件把Agent拆开看其实就四样东西大脑一个大模型负责理解用户意图、做规划、决定下一步做什么。可以用云端API也可以本地部署。手一组工具Functions比如查天气、查数据库、发邮件、调内部接口。模型不会真去执行但会“点名”要调用哪个。记忆短期记忆是对话历史让Agent记得上一句说了什么长期记忆是向量数据库或数据库让Agent记住上周你说过的偏好。工作循环模型输出决策系统执行工具结果返回给模型模型再决策循环往复直到任务完成。这是Agent区别于ChatBot最核心的机制。这四个零件缺一个Agent的能力都会大打折扣。没有工具的Agent只能纸上谈兵没有记忆的Agent每次对话都像第一次见面没有工作循环的Agent只能做单轮问答。1.3 ReAct循环让模型“干完活再说话”的最小原理ReAct这个词是Reasoning Acting的组合意思是让模型交替进行“推理”和“行动”。整个循环大概长这样用户给一个目标。模型输出推理过程Thought和行动指令Action比如“我需要查一下北京的天气调用get_weather(city北京)”。系统执行这个工具拿到真实结果Observation。把结果返回给模型。模型根据新信息继续推理要么调用下一个工具要么直接给出最终答案。关键点在于工具执行的结果是真实的、来自外部系统的不是模型自己编的。这就把模型从“靠记忆编答案”变成了“靠工具拿答案”。我最早自己裸写ReAct循环时光解析模型的输出就调了好几天——因为模型一会儿输出JSON、一会儿输出Markdown、一会儿忍不住想跟你聊天。后来我才明白这不是模型不听话而是自己没有用正规的Function Calling机制去约束它。这个后面会专门讲。2. 技术选型从裸写循环到LangGraph我踩过的弯路2.1 为什么我不建议“裸写”ReAct循环先说说我最初的做法不依赖任何框架直接调大模型接口自己在代码里维护一个messages数组循环里把工具结果append进去然后判断模型输出里有没有“调用工具”的标志再继续下一轮。这个方案对小Demo完全可行尤其是只调一两个工具的时候代码甚至比框架还短。但一旦工具多起来、分支逻辑变复杂问题就来了状态散落在各个变量里、调试时要手动打印每一轮的中间结果、上下文一长token就开始失控、还要自己处理工具异常和重试。更尴尬的是一旦要支持多Agent协作这套散装的代码几乎没法扩展。我自己在这个阶段浪费了大概两周。所以我的建议是如果你只想验证一个想法裸写没问题但凡打算做成一个能长期维护的东西直接上编排框架。2.2 主流框架对比目前市面上做Agent编排的主流方案我用下来大概是这么个情况方案优点缺点适合场景裸写ReAct循环灵活、无依赖、容易理解状态管理混乱、扩展性差学习原理、极简DemoLangChain Agent生态成熟、组件多抽象层多、调试困难快速原型验证LangGraph图为模型、状态清晰、可控性强有一定学习成本生产级复杂Agent自研调度引擎完全可控、贴合业务开发周期长大厂特殊业务场景我现在的选择是LangGraph。它的核心思路是把Agent运行过程画成一张有向图节点是操作比如“调用模型”“执行工具”边是流转条件状态集中管理。这种设计天然适合单Agent、多Agent、人工介入、条件分支这些场景而且每一步都显式可见调试体验比LangChain的Agent链式调用好太多。2.3 环境准备一套能立刻跑起来的依赖我推荐直接用Python虚拟环境版本3.10以上比较稳。核心依赖其实就四个pip install langchain-openai langgraph langchain-core python-dotenv再加一个你自己选的大模型SDK比如OpenAI的官方包。如果你的网络环境没法直连官方API国内不少厂商提供了OpenAI兼容接口只需要改base_url和api_key代码其余部分基本不用动。API Key的配置我习惯用.env文件管理而不是直接写在代码里OPENAI_API_KEY你的key OPENAI_API_BASE你的接口地址可选然后在代码里这样加载import os from dotenv import load_dotenv load_dotenv() model ChatOpenAI( modelgpt-4o-mini, temperature0, )这里有个小建议开发调试阶段temperature设成0让模型的输出尽量稳定不然同一个问题跑两次结果不一样排查Bug时会很痛苦。等上线后再根据场景调整。3. 第一个能“干活”的Agent查天气报时30分钟跑通3.1 工具先行两个函数就是一个Agent的“手”第一个Agent我只加了两个工具查天气和查当前时间。工具本质就是Python函数用tool装饰器标记一下LangGraph就能识别。from datetime import datetime from zoneinfo import ZoneInfo from langchain_core.tools import tool tool def get_weather(city: str) - str: 查询指定城市的当前天气输入中文城市名即可。 weather_map { 北京: 晴24到32摄氏度微风, 上海: 小雨26到30摄氏度东南风3级, 深圳: 多云28到33摄氏度西南风2级, } return weather_map.get(city, f抱歉暂时没有{city}的天气数据。) tool def get_current_time(timezone: str Asia/Shanghai) - str: 获取指定时区的当前时间默认北京时间。 now datetime.now(ZoneInfo(timezone)) return f{timezone}当前时间是 {now.strftime(%Y-%m-%d %H:%M:%S)}注意两个细节。第一函数名就是工具名必须是英文小写加下划线第二docstring必须写清楚这个工具是干什么的、参数是什么格式因为模型会根据这个描述决定什么时候调用它。描述写得含糊模型就会乱调。3.2 用LangGraph把Agent拎起来有了工具接下来用LangGraph的create_react_agent把这个Agent组装起来from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent model ChatOpenAI( modelgpt-4o-mini, temperature0, ) agent create_react_agent(model, tools[get_weather, get_current_time]) response agent.invoke({ messages: [(user, 明天要去北京开会帮我看看北京天气怎么样顺便告诉我现在几点。)] }) print(response[messages][-1].content)就这么几行一个能真正“调用工具”的Agent就跑起来了。create_react_agent内部自动帮你实现了ReAct循环模型的输出会被检查有没有工具调用请求有就执行对应函数再把结果塞回消息列表继续下一轮。3.3 第一次运行Agent到底在背后做了什么很多人跑通之后不知道里面发生了什么这里我把过程展开给你看。当用户问“北京天气怎么样、现在几点”时Agent内部实际经历了好几轮模型读到用户消息决定调用get_weather和get_current_time两个工具输出两个工具调用请求。LangGraph执行这两个函数拿到“晴24到32摄氏度”“北京时间是某年某月某日”这样的真实结果。把工具结果作为Observation消息返回给模型。模型看到结果后生成最终的自然语言回复整理成对用户友好的说法。整个过程模型不是在“背书”而是真的拿到外部函数返回的数据之后再组织语言回答。这也是Agent和ChatBot本质的区别ChatBot在编Agent在查。3.4 实测最容易翻车的三个地方第一次跑通不代表能稳定跑我在测试阶段遇到过几个反复出现的坑模型反复调用同一个失败的函数。原因是工具返回的错误信息不够明确。后来我给工具返回的内容加了结构化前缀比如{status: 400, message: 城市不存在}模型一眼就能懂。Agent陷入无限循环。比如用户问的问题模糊模型反复调用“查询”类工具却得不到答案。LangGraph默认有递归步数上限超过会抛异常。生产环境我通常显式设置recursion_limit15左右防止一个Bug让Agent空转到破产。上下文越积越长。每轮工具调用的输入输出都会累积到消息列表里跑几次大任务后token消耗会非常可观。关于怎么处理我会在第5章专门说。4. 手和脑之间Function Calling的正确打开方式4.1 模型不是执行者它只是“调度员”这个点值得再强调一遍大模型本身不会调用任何工具它做的是“调度”工作——根据用户需求和工具描述决定“现在该用哪个函数、传什么参数”然后把决策以结构化的方式输出。在你调的API层面模型是多输出了一段tool_calls字段里面包含函数名和参数JSON。真正执行函数的是你的代码。也就是说Agent的“手”长在你的代码里模型只是那个发号施令的“大脑”。这个认知一旦建立很多设计问题就顺了。比如一个工具函数里写多少业务逻辑、怎么处理DB事务、怎么保证幂等这些都是程序员自己负责的模型不管。模型只负责“选对工具、传对参数”。4.2 工具描述写得好不好直接决定Agent蠢不蠢工具描述就是模型做决策的依据相当于操作手册。我见过很多Agent表现拉垮不是模型不行而是工具描述写得一塌糊涂。以一个最简单的函数为例内部传给模型的工具Schema长这样{ name: get_weather, description: 查询指定城市当天的天气情况支持国内主要城市, parameters: { type: object, properties: { city: { type: string, description: 中文城市名例如北京、上海、广州 } }, required: [city] } }写工具描述时有几个经验description里说清楚“什么时候用”。比如“当用户询问天气、出行建议、户外活动安排时调用”。这样模型更容易把它和用户意图关联起来。参数description要写示例值。模型对“能少思考就少思考”有强烈的偏好你给它示例它大概率直接用。一个工具只做一件事。别做一个“万能查询函数”参数里又是类型、又是日期、又是页码模型会晕还会传错参数。工具数量控制在合理范围。有些人的Agent一口气挂了二三十个工具模型每轮都要在二十几个工具里做选择准确率会肉眼可见地下降。能合并的工具就合并。4.3 工具报错了怎么办把错误喂回模型工具执行时抛异常是常态比如下游接口超时、数据库连不上、参数不合法。我见过最原始的处理方式是程序直接raise整个Agent崩掉。这是一种浪费因为大多数情况下模型自己就能修正。正确做法是捕获异常把结构化错误信息作为工具结果返回给模型让模型根据错误信息调整策略继续行动。我常用的一个写法tool def query_user_order(order_id: str) - str: 查询用户订单详情输入订单号 try: order order_service.query(order_id) return f订单信息{order} except OrderNotFoundException: return json.dumps({error: ORDER_NOT_FOUND, message: 订单号不存在请检查后重试}) except ServiceUnavailableException: return json.dumps({error: SERVICE_BUSY, message: 订单服务暂时不可用})这样模型读到ORDER_NOT_FOUND就知道要重新向用户要一个正确的订单号读到SERVICE_BUSY就知道可以稍后重试或者让用户等一等。Agent不会因为一次简单的异常就断掉体验会好很多。4.4 MCP工具调用的“USB接口”讲工具调用就不能不提MCPModel Context Protocol。如果说工具是Agent的“手”那MCP就是给手装的“USB接口”——把工具调用标准化成了一个协议。以前每个Agent接一个外部系统就要写一套私有集成N个系统写N套。MCP出现之后服务方只需要实现一个MCP Server暴露工具清单Agent端用统一的MCP Client连接就能自动发现和调用这些工具。一次接入到处复用。我的建议是如果你做的是ToB场景内部系统多、对接方多直接上MCP能省下巨大的重复开发量。如果是个人项目、只有两三个自建工具没必要硬套MCP写了反而多一层复杂度。5. 记忆设计Agent记不住事什么能力都是白搭5.1 短期记忆和上下文窗口的取舍大模型API本身是无状态的它不记得你上一句话说了什么。所谓的短期记忆其实就是把之前的对话消息拼在请求里一起发过去。最笨的办法是无限累积历史消息。跑几轮之后你就发现上下文窗口被撑爆或者费用肉眼可见地涨。我的做法是给历史消息设一个硬上限比如最近20条超过的部分做摘要压缩用一个几百字的“历史要点”替换掉老消息。还有一种更精细的方式按消息角色和用途做窗口管理。比如系统提示词永不丢、工具执行结果保留最近的5轮、用户消息保留最近的10轮。具体看业务场景核心思路就是“能省则省但关键的别丢”。5.2 长期记忆向量检索才是正经方案短期记忆解决“上一句说了什么”长期记忆解决“上次聊过什么、用户偏好是什么”。实现上我推荐向量数据库比如Chroma、Qdrant或者Milvus按需选就行个人项目用Chroma就很舒服零配置纯本地跑。向量库存的核心逻辑是把需要记住的内容做Embedding转成向量存进去下次用户提问时把问题也做成向量在库里做相似度检索召回相关的历史记录。这里要特别提醒一句不要把所有的历史对话原文一股脑存进去。存之前先做一次“记忆抽取”只保留有价值的信息比如用户偏好、未完成的待办、重要的事实。原文存多了检索噪音大token浪费也多效果反而更差。5.3 一个完整的记忆存取流程我目前的实现思路是这样的供你参考。用户第一次说“我喜欢简洁的回答不要太啰嗦”这句话本身不是记忆但“用户偏好简洁回答”就是值得长期记住的信息。存取过程分为四步对话结束时用一个专门的抽取流程可以再调一次大模型判断对话里有没有值得长期记住的信息有就保留没有就丢弃。把值得记的信息用Embedding模型转成向量。写入向量库带上用户ID作为元数据保证不同用户的记忆隔离。下一次对话开始时用当前用户的问题去向量库检索相关记忆把命中的内容注入系统提示词。伪代码大致是这样import chromadb client chromadb.PersistentClient(path./memory_store) collection client.get_or_create_collection(nameagent_memory) def save_memory(user_id: str, content: str): embedding embed_model.embed_query(content) collection.add( ids[f{user_id}-{hash(content)}], embeddings[embedding], documents[content], metadatas[{user_id: user_id}] ) def recall_memory(user_id: str, query: str, top_k: int 3): query_embedding embed_model.embed_query(query) results collection.query( query_embeddings[query_embedding], where{user_id: user_id}, n_resultstop_k ) return results[documents]Embedding模型我自己的经验是中文场景用bge-m3系列效果不错英文场景用OpenAI的text-embedding-3-small就够了。不需要追求最大的模型性价比和效果之间取平衡更重要。6. 当任务复杂到单Agent搞不定多Agent协作的正确姿势6.1 什么情况下真的需要多Agent先说结论一个Agent能解决的绝对不要上两个。多Agent不是炫技它带来的收益是任务分解、专业化处理但代价是成本翻倍、复杂度飙升、调试地狱。我自己判断是否使用多Agent一般看三条单个Agent的System Prompt太长角色混杂一会儿当规划者一会儿当执行者模型容易精神分裂。子任务之间需要不同的模型配置比如一个任务既要快速分类、又要深度推理分为两个Agent分别用不同模型更划算。任务可以并行处理多个子任务独立进行用多Agent能提速。如果符合其中一两条可以考虑多Agent如果都不符合老老实实用单Agent。6.2 规划者执行者最经典的分工模式多Agent协作最经典的模式就是Supervisor模式一个规划者Supervisor负责理解全局、分配任务、汇总结果几个执行者Worker各自负责一个专业领域。举个例子让Agent完成“做一份竞品分析报告”。规划者先拆解竞品信息收集、数据分析、报告撰写三个子任务。然后分别派发给三个Worker最后汇总成一份完整报告。用LangGraph来搭这个结构核心是定义几个节点和一个路由逻辑。示意结构大致如下from langgraph.graph import StateGraph, MessagesState, START, END async def planner_node(state: MessagesState): # 调用大模型决定下一步任务派给谁 ... async def collect_worker(state: MessagesState): # 竞品信息收集 ... async def analyze_worker(state: MessagesState): # 数据分析 ... async def write_worker(state: MessagesState): # 报告撰写 ... def router(state: MessagesState): # 根据planner的决策返回下一个节点的名字 ... g StateGraph(MessagesState) g.add_node(planner, planner_node) g.add_node(collect, collect_worker) g.add_node(analyze, analyze_worker) g.add_node(write, write_worker) g.add_edge(START, planner) g.add_conditional_edges(planner, router) g.add_edge(collect, planner) g.add_edge(analyze, planner) g.add_edge(write, END)核心思想是每个Worker干完活都回到规划者那里由规划者判断任务是否完成、要不要继续派单。这样整体流程就是可控的而不是几个Agent各干各的最后撞在一起。6.3 多Agent协作最容易翻车的三个地方多Agent看着热闹落地全是坑。根据我自己的项目经验最大的三个坑状态传递混乱。多个Agent共享一份状态谁改了谁的字段根本说不清。我的对策是定义清晰的状态结构每个Worker只允许读写自己负责的字段。Token成本失控。一个多Agent任务跑一次光是规划者来回调度就好几次再加上各个Worker的输入输出消耗比单Agent高出十倍都不奇怪。上多Agent之前一定要想清楚回报率。死循环。规划者把任务分下去Worker干完回来规划者又觉得不满意再派下去来回拉扯好几次才算完。LangGraph里要设置步数上限和“不满意次数上限”超过就强制输出当前结果引入人工介入。7. 上线前的最后一公里生产级Agent的硬指标7.1 超时、重试和幂等不能让Agent“重复干活”开发环境跑通Agent和线上稳定运行之间隔着一大堆工程细节。最容易被忽略的是“Agent不是只有一次API调用而是多次循环调用”这意味着任何一步出问题整个任务就卡住了。我的建议是三层防护每一层都设超时。大模型调用设5到30秒超时工具调用设更短的超时。任何一个环节卡死整个任务就应该及时失败并返回可读的错误信息。重试要带指数退避。大模型接口偶尔抖动是正常的直接重试经常能解决。但不能每秒重试一次要退避比如1秒、2秒、4秒这样递增。工具要幂等。这个很多人会忽略。Agent跑一半断了你重试任务工具可能被重复执行。比如一个“创建订单”的工具重试会导致重复下单。解决办法是让工具接收一个调用端的请求ID服务端按ID做去重。7.2 防幻觉和防跑偏关键操作要不要人确认Agent在自由发挥的过程中偶尔会编造工具结果、编造数据、甚至自说自话地往某个方向跑偏。这是大模型的天性无法根除只能靠设计约束。我的做法是分级处理查询类、分析类行动允许自主执行但最终答案必须基于工具返回的真实数据不允许模型凭记忆补充细节。写操作尤其是下订单、删数据、发消息这类不可逆操作强制加入“人工确认”环节。Agent先把要执行的指令整理好展示给用户用户点了确认才真正执行。关键数据输出要求工具在返回结果时带上数据来源或时间戳模型最终回答时引用这些来源降低编造的概率。生产环境里安全永远比智能重要。一个偶尔显得“笨”但在关键操作上绝不越界的Agent远比一个成天自作主张的Agent更靠谱。7.3 token成本与模型选型本地部署还是云端API跑一个Agent的token消耗经常让第一次接触的人很吃惊。一个复杂任务可能循环调用模型十几次每次几千token跑一次下来消耗几十万token都是家常便饭。我控制成本的办法总结下来三条任务分级用模型。简单分类、信息抽取用便宜的mini系列复杂推理、规划决策用大模型。规划者用贵的执行者用便宜的整体成本立刻降下来。缓存重复结果。相同的问题和相同的工具调用结果做好缓存。企业内部经常有同样的Query反复跑缓存省下的钱相当可观。本地部署看需求。如果数据敏感不能出内网或者token量极大、长期算下来比买GPU更贵那就用Ollama或vLLM在内部部署开源模型。vLLM在高并发场景下吞吐更好Ollama胜在部署简单。唯一要提前做好心理准备的是开源小模型在工具调用准确率上跟顶尖商用模型还是有差距需要在Prompt和工具描述上多花功夫弥补。我在实际开发中的体会是Agent的每一步“智能”其实都等价于一次模型调用而每次调用都是钱。成本不是写代码时考虑的事而是架构设计时必须算清的账。如果你问我做Agent这段时间最大的感受我会说Agent开发最难的不是让模型变聪明而是“约束”。模型太聪明容易跑偏模型太笨干不了活。你要做的就是给它配好工具、画好跑道、设好护栏然后盯着它跑完全程。这个平衡点没有标准答案只能靠一次次真实业务里的踩坑去磨。希望这篇指南能让你少踩几个我踩过的坑。