直接说结论:大模型Agent(智能体)开发,本质上是教一个“话多但没法动手”的大模型学会使用工具、记住上下文、拆解任务,并且在一个循环里把活儿干完。你给它一个目标,它自己规划、调API、读文档、做判断,最后把结果交给你。这跟传统“写死逻辑”的程序完全是两种思路——传统代码是你在控制一切,Agent是你在定规则和目标,剩下的事让模型自己走。
我最早接触Agent是在一个企业内部知识库项目里,当时的需求很朴素:用户问问题,系统要自己判断是查数据库、搜文档还是直接回答,还要在查完以后把多份材料汇总成一段人话。最初我试图用if-else把所有路径写死,结果分支组合爆炸,维护到怀疑人生。后来换了思路,让模型自己决定每一步做什么,只给它工具、边界和反馈信号,整个系统的灵活性和准确率反而明显提升。这就是Agent的核心价值:把“程序员的判断逻辑”转移成“模型的推理循环”。
这篇文章适合所有刚接触大模型Agent开发的人,无论你是后端、前端、运维还是算法转过来,只要会用Python(或TypeScript),理解基础的HTTP调用,就能跟完整条路线。我会把从环境搭建、核心机制、代码实现到常见坑位的完整过程都过一遍,并且重点讲清楚“为什么这样做”——参数为什么这么设,循环为什么这么写,缓存为什么要有,这些才是纸上文档不会告诉你的东西。
1. 先拆清楚:你要做的到底是什么东西
1.1 Agent和普通API调用的边界在哪里
很多人以为Agent就是“调用大模型API,把用户问题发过去,拿到回复”。这是最大的误区。纯粹的API调用是单向的:你给一个Prompt,模型给一个回答,交互一轮就结束,模型不保留状态,不调用工具,也不会自己去验证结果。
Agent是另一套结构。它的核心是“感知-决策-行动-观察”的循环:模型根据当前状态做决策,决定调用哪个工具;工具返回结果;模型再根据结果更新状态、做下一步决策,直到认为任务完成。典型结构包括:LLM(大脑)、工具集(手脚)、记忆(短期+长期)、规划器(任务拆解和步骤编排)。
拿订机票举例。普通API调用的方式是:你写一个函数,用户选城市、日期,你调航司接口,返回航班,展示列表。Agent的方式是:你告诉Agent“帮我订一张下周三从上海到北京的机票,预算一千五以内”,Agent自己决定先查航班、再比价、选符合预算的班次、然后调用下单接口,中途如果发现预算内没航班,它还会自己调整策略,比如问你要不要改时间,或者把预算放宽。这个“自己决定怎么做”的闭环,就是Agent的灵魂。
1.2 两个核心问题:什么该让模型决定,什么该用代码锁死
这个问题想不清楚,Agent项目基本会失控。
我踩过最大的坑就是“什么都让模型决定”。最开始做Agent的时候,我把工具选择、参数校验、结果校验全部交给模型,结果模型在简单的查询任务里表现出色,一到真实业务场景就频繁出错:要么把参数格式传错,要么工具返回错误后不断重试同一个动作,要么在几个选项之间反复横跳。
后来我总结出一个相对靠谱的边界原则:
- 高风险、强约束的步骤用代码锁死,比如金额计算、权限判断、数据合规校验、删除操作前的确认流程,这些地方必须写死规则,模型只有执行权没有决定权。
- 低风险、多路径的步骤让模型决定,比如查资料的方式是搜索还是读库、多份结果如何取舍、答案如何组织表达,这些灵活性问题交给模型反而效果更好。
- 中间过程要可观测可干预,每一步都留日志,关键节点设置人工确认位。
总的设计哲学是:Agent负责“怎么走”,代码负责“哪里不能走”。这样既保留了大模型的灵活性,又避免失控。
2. 开发环境与选型:动手前的关键决策
2.1 技术栈怎么选:框架派还是自研派
市面上的Agent框架很多,常见的包括LangChain、LangGraph、AutoGen、CrewAI、以及国内一些团队封装的框架。我自己的经验是:入门阶段不要一上来就套重型框架。先用原生代码把“模型循环+工具调用”的逻辑跑通,理解整个机制,然后再看框架能帮你省掉什么。
为什么这么说?因为框架会隐藏大量细节。比如LangChain的AgentExecutor,你调用agent.run(),它内部帮你做了Prompt组装、工具调度、历史压缩、错误处理。看起来很省事,但实际出问题时你根本不知道是哪一环出了问题。我见过很多同事调框架Agent调不通,翻源码翻到怀疑人生,最后退回原生写得明明白白。
如果是开发一个面向生产的小型Agent,我个人建议的技术栈组合是:
- 语言:Python 3.10以上,生态最成熟,工具函数写起来最顺手。
- 模型层:优先考虑支持Function Calling/Tool Use的模型,比如OpenAI系、Claude系、国内的通义千问、智谱GLM、DeepSeek等,都可以通过OpenAI兼容接口调用。只要兼容就好办,后续换模型只改base_url和model名。
- 框架:可以不装或只装轻量的。我入门时用纯Python写循环,后来接LangGraph管理复杂多节点流程,再后来发现很多业务其实用不上图结构,一个
while循环加条件判断就够了。 - 工具层:HTTP请求用
httpx,结构化解析用Pydantic,记忆存储用SQLite或Redis,日志用loguru。
如果你确实是团队协作、需要多人快速搭建复杂Agent应用,选LangGraph或者字节的Coze(如果你不想写代码)也没问题。但从学习角度,我强烈建议至少自己写一个最小Agent循环。
2.2 模型选型:不是越贵越好,而是越合适越好
模型选型上,很多新手有一个误区:只要效果不好就换更大更强的模型。实际上,Agent场景下更重要的指标是:工具调用的稳定性、上下文长度、响应速度和成本。
工具调用稳定性是最关键的。Agent每轮循环都要判断“该调用哪个工具、参数是什么”,如果模型不稳定,JSON参数经常漏字段或格式错误,整个Agent就跑不起来。目前实测下来,OpenAI的GPT-4o系列、Claude Sonnet系列、Qwen的max系列表现都比较稳;轻量模型在简单场景也够用,但一旦工具多了(十几个以上),选型就要谨慎。
上下文长度决定了Agent能“记住”多少过程。Agent每一步都会把工具结果拼进对话历史,如果模型窗口只有8K,跑几步就满了。建议最低选32K以上,64K或128K更好。
成本和速度也要算账。一个复杂任务Agent可能要调用模型十几轮,每一轮都是tokens。如果每轮都发顶级模型,一次任务可能几块钱,生产环境根本扛不住。我的建议做法是分级路由:简单任务走轻量模型(如DeepSeek、Qwen-turbo),复杂推理才升级到旗舰模型。很多框架支持自定义路由逻辑,自己写也就几十行代码。
到手的环境配置很简单:拿到API Key,确认接口格式为OpenAI兼容格式,然后设置环境变量。本地开发时,我习惯把配置写在.env文件里,通过pydantic-settings加载,而不是散落在代码各处。
# .env 示例 OPENAI_BASE_URL=https://your-model-endpoint OPENAI_API_KEY=sk-your-key MODEL_NAME=qwen-max注意:不要把API Key提交到git仓库,尤其项目准备开源时。我见过不止一次把Key硬编码进代码然后被薅成羊毛的案例。用环境变量或者密钥管理服务,这是基本常识。
2.3 最小闭环:10分钟跑通一个Agent
不废话,先看代码。这是一个用原生Python实现的最简Agent循环,支持让模型反复调用一个“查询天气”的工具,直到用户满意:
import json import httpx # 假设你的模型服务兼容OpenAI接口 OPENAI_BASE_URL = "https://your-model-endpoint/v1" API_KEY = "sk-your-key" MODEL = "your-model" client = httpx.Client( base_url=OPENAI_BASE_URL, headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30.0, ) def call_llm(messages): resp = client.post("/chat/completions", json={ "model": MODEL, "messages": messages, "tools": tools, "tool_choice": "auto", }) return resp.json()["choices"][0]["message"] def get_weather(city: str) -> str: """模拟查询天气的工具,正常情况这里应该调真实天气API""" # 这里写真实逻辑,比如请求某个天气服务 return f"{city},晴,23℃~31℃,东北风3级" # 工具描述,大模型会根据这套描述决定何时调用 tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当前的天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如北京、上海" } }, "required": ["city"] } } } ] # 工具注册表:模型说要调什么,我们就执行什么 tool_map = { "get_weather": get_weather, } messages = [ {"role": "system", "content": "你是天气助手。查询天气后,用自然语言把结果告诉用户。"}, {"role": "user", "content": "北京今天天气怎么样?适合穿什么?"}, ] MAX_ITER = 5 for i in range(MAX_ITER): message = call_llm(messages) # 第1步:模型决策 messages.append(message) if not message.get("tool_calls"): # 模型没有要求调用工具,说明它已经可以直接回答 break # 第2步:依次执行模型要求的工具 for tool_call in message["tool_calls"]: fn_name = tool_call["function"]["name"] fn_args = json.loads(tool_call["function"]["arguments"]) result = tool_map[fn_name](**fn_args) # 第3步:把工具结果作为一条新的历史消息,回传给模型 messages.append({ "role": "tool", "tool_call_id": tool_call["id"], "content": result, }) print(messages[-1]["content"])这个代码就是Agent的骨架,只有几十行。整个循环分三步:模型决策、执行工具、返回观察结果。你把它跑通,就掌握了Agent最核心的主循环。在此基础上加记忆、加RAG、加并发,都只是往这个骨架上长肉。
3. 核心机制逐个击破:工具、规划、记忆与安全
3.1 Function Calling是Agent的立足之本
Agent“手脚”的本质就是让模型在对话中对你说:“我想调用这个函数,参数是这些。”你的代码负责接收这个意图,执行真实逻辑,再把结果注入对话。这个过程业界叫Function Calling或Tool Use。
经验上,工具描述怎么写,直接决定Agent的成功率。我总结出三个实用原则:
- 描述里讲清楚什么时候该用。比如
get_weather的描述不要只写“查询天气”,要写“当用户询问某地当前天气或未来天气预报时使用,城市名必须是中文名称”。 - 参数名和英文名要直观。不要出现
a、b这种无意义命名,模型看不懂,参数直接暴露给Agent,命名越语义化越不容易传错。 - 少即是多。工具数量不是越多越好。我见过一个项目注册了30个工具,模型频繁选错。每多一个工具,模型决策的空间就大一分,出错概率也大一分。能用5个工具解决问题的场景,就不要注册15个。
实测数据也验证这一点:在十几个工具的场景下,模型偶尔会张冠李戴,比如把查询A接口的参数传给B接口。所以工具设计时要尽量职责单一,参数命名规范和文档描述充分,必要时还要在代码里做一层参数归一化处理,把模型传出来的参数清洗后再去调真实服务。
3.2 ReAct循环:让Agent学会“边想边做”
ReAct(Reasoning + Acting)是Agent最经典的模式之一,核心逻辑是让模型在每一轮循环中先输出自己的想法,再决定行动。虽然现在很多模型API把“思考过程”隐藏了,但你可以把系统提示词设计成一种隐性ReAct:要求模型输出“当前状态判断 -> 下一步行动 -> 等待观察结果”的结构。
我设计Prompt的时候会这样要求:
你的工作方式是循环式的:分析当前对话和已知信息,决定是否需要工具;如果需要,只输出一个工具调用指令,等待工具返回结果后继续;如果信息已经足够,直接输出最终答案。不要在一次回复中既调用工具又给最终结论。
这个要求非常重要。很多Agent翻车就是因为模型在还没拿到工具结果时就抢跑,直接编了一个答案给你。在开发中,你需要配合代码逻辑做约束:只要模型返回了tool_calls,就不输出最终答案;只有没有tool_calls时才输出答案。
整个循环的退出条件有三个,缺一不可:
- 模型不再要求调用工具,并且消息里包含了明确的最终回答。
- 达到最大迭代次数(这个必须有,防死循环)。
- 触发停止条件或人工中断。
迭代次数我一般设5到8轮,复杂的多步骤任务偶尔要15轮以上。注意每多一轮就是多次模型调用,成本和耗时线性上涨。遇到需要几十轮才能完成的长链路任务,建议拆成多个子Agent分阶段处理,而不是在单循环里硬跑。
3.3 记忆系统:短期靠上下文窗口,长期靠外部存储
Agent如果记不住东西,就跟金鱼一样,聊两句就断片。
短期记忆其实不用你额外开发,把所有历史消息通过messages数组传给模型就行。这里唯一要注意的是上下文窗口的截断策略。当消息太多超出窗口限制,你要决定丢弃哪些、压缩哪些。最简单的方法是只保留系统提示词和最近N轮对话,中间细节丢了就丢了。进阶做法是用模型对历史消息做摘要,把摘要作为一条新消息替换掉旧对话。我实践中更倾向“滑动窗口 + 关键信息抽取”的组合:窗口只保留近几轮全文,更早的内容提取成结构化摘要(比如用户已经提供的偏好、尚未完成的事项)存进上下文。
长期记忆则需要外部存储。常见做法是把用户的信息、业务规则、历史结论写入数据库或Redis,当下次与该用户交互时,先把这些信息检索出来拼接进系统提示词。这本质上就是RAG(检索增强生成)的变体。长期记忆我强烈建议做“结构化优先”:能存字段的存字段,不要一股脑把对话流水都塞进去。比如用户偏好存成JSON字段,Agent启动时读取并注入,比每次从对话里“猜”稳定得多。
3.4 安全护栏:给Agent装刹车和防火墙
Agent是“让模型自己做决定”的系统,所以安全必须从架构层面考虑,不能指望模型自律。
我把护栏分成三层:
- 输入护栏:用户的输入先经过简单校验和分类,比如是否包含恶意指令、是否涉及敏感操作、是否属于当前Agent职责范围。这一步用关键词+轻量模型判断就能做。
- 工具护栏:不是所有工具都无条件开放给模型。我的做法是给每个工具打上“风险等级”。低风险的直接放行;中风险的加上参数白名单校验;高风险的必需二次确认,模型只能输出“准备执行某操作”,真正执行要用户点头。
- 输出护栏:模型生成答案后,用规则或另一个模型扫描一遍,防止泄露内部信息或诱导内容。过去很多Agent安全事件都是出现在“输出侧”,比如模型被诱导说出不该说的内容。
另外一个容易被忽略的点是Agent的“试错成本”。真实系统里,Agent每一次工具调用都是有副作用的,比如扣款、发消息、改数据。你在开发环境跑无所谓,上生产前一定要给Agent套一层“模拟模式”:所有工具走mock实现,日志完整记录,验证无误再切换真实执行。
4. 让Agent能干重活:检索增强、并发与工程化
4.1 RAG增强:给Agent接上私有知识库
大模型的训练数据不可能包含你公司的内部文档,所以让Agent懂业务,必须做检索增强。
简单来说,RAG流程是:把文档切片、向量化、存入向量数据库;用户提问时,把问题向量化,检索出最相关的若干片段;拼接进Prompt,让模型基于这些片段回答。在Agent场景里,我会把“检索”封装成一个工具,而不是放在主流程外面。
为什么封装成工具更好?因为Agent会自己判断“这个问题要不要查资料”。比如用户问“咱们公司年假怎么休”,Agent可能先调检索工具查制度文档;而用户问“你好”,Agent就直接聊天,完全不去查库。相比于固定先检索再回答的Pipeline,Agent方式更省token、更灵活。
工程上有几个细节值得注意:
- 切片策略:不要按固定字数硬切,最好按章节或段落语义切,重叠部分设个一两百字符,防止关键上下文被切断。
- 向量模型要跟检索场景匹配:中英文混合场景不要纯用英文向量模型,国产向量模型(如BGE系列)中文效果明显更好。
- 召回后要重排:初召回取二三十条,用一个轻量重排模型挑出最相关的五六条,效果提升非常明显,成本也不高。
- 引用溯源:Agent回答时尽量带来源标识,方便用户核对,也方便排查。
4.2 并发与性能:Agent怎么扛住压力
“AI Agent怎么扛并发”是网上问得非常多的问题。Agent和传统接口不一样,一个复杂的Agent请求可能要好几秒甚至几十秒,内部还多次调用模型API。如果你按同步方式处理,一个用户占用一条线程,几十个用户同时来,线程池就炸了。
我实践下来,扛并发的核心思路是三层:
- 接入层用异步:Python端用
asyncio包装Agent调用,并发请求走消息队列或者异步任务框架。如果Agent内部是同步的,至少用asyncio.to_thread把阻塞调用丢到线程池,避免事件循环卡死。 - 模型API层要限流和重试:所有模型服务都有速率限制,并发一高,429(请求过多)必然出现。我的做法是用一个全局的限流器(比如
slowapi、Redis令牌桶),同时给调用加指数退避重试。重试要小心,模型调用不是幂等的,用户可能已经收到部分回答,所以最好在重试策略里加上“生成本轮结果,失败则整体返回错误并让前端提示重试”。 - 缓存一定要做:相同或相似的问题,如果Agent答案可以复用,就不要每次都烧模型。我在项目里用语义缓存:先算用户问题的向量,查最近半小时内有没有相似问题(余弦相似度>0.95),有就直接用缓存答案,没有才走Agent。实测这个策略能挡住30%甚至更多的重复流量。
另外一个容易忽略的性能点是流式输出。Agent内部要调多轮模型,如果每一轮都是非流式的,用户看到的就是一个转圈N秒、然后一次性吐出一大段文字,体验很差。我建议最终答案的输出用SSE(Server-Sent Events)逐字推送,中间的工具调用过程可以在前端展示成一个“思考过程”面板。工具调用步骤本身不用流式,因为那是给系统看的,但可以异步推送给前端做状态展示。
4.3 可观测性:日志、追踪与评估,缺一不可
Agent项目上线后最痛苦的时刻,就是你明知道它某个地方错了,但根本不知道是哪一步错的。所以我强烈建议,一开始就把可观测性做好,不要等出了问题再补。
我在每个Agent项目里都会做这么几件事:
- 结构化日志:每一轮循环输出一条JSON日志,记录当前消息数、是否触发工具调用、工具名、参数、返回结果摘要、耗时、累计tokens。日志字段统一,后面查问题直接过滤。
- 链路追踪:如果用了框架,开启LangSmith或Langfuse这类追踪工具;自研的话给每次请求生成一个
trace_id,贯穿所有日志和上下行调用。 - 自动评估集:准备一套测试用例(50到100条),每次改动Prompt或工具逻辑后,批量跑一遍Agent,对比输出质量。人工看几十条Agent回答太费时间,可以引入一个打分模型做初筛,人工只检查分低的。没有这一步,你根本不敢改代码。
我记得有一次改了一个工具描述,单个测试用例的通过率从80%掉到60%,要不是有自动评估集,我根本不会注意到这个微小的退化。Agent系统的脆弱性远比传统代码高,一处Prompt措辞的调整都可能影响全局行为,所以回归测试是不可省略的环节。
5. 高频问题与排查技巧实录
5.1 模型就是不调用工具,怎么办
这是Agent开发中最常见的问题。排查顺序是:先看Prompt里工具描述是否清晰,再看模型本身是否支持Tool Use。
具体说,我遇到过几次:
- 模型版本不支持Function Calling,你传了
tools参数它直接无视。 - 工具描述太模糊,模型不知道“这事归我管”,把该调工具的问题当闲聊回答了。
- 系统提示词和工具描述冲突,比如你告诉模型“你是闲聊助手”,然后又给它检索工具,它自然会倾向于不调用工具。
tool_choice设置问题,有些接口支持required,可以强制模型必须调用工具。调试时可以先设required看工具链路通不通,通了再改回auto。
还有一种情况是模型想调用工具,但你的代码处理消息的方式不对——比如直接把模型返回的整个消息塞进历史,而没把tool_call_id对应上,下一轮模型就会懵。
5.2 工具参数总是传错或格式非法
这个问题的根因通常在工具定义。我经历过的是:参数描述和真实代码要求不一致。比如代码里要求日期格式YYYY-MM-DD,但工具描述里只写了“日期”,模型就可能传“明天”或者“2024年2月1日”。解决办法是:在参数描述里把格式写死,并且在执行工具前加一层清洗函数做兜底解析,能转就转,转不了就返回错误,让模型自己修正。
另外一个高频问题是模型返回的参数JSON带着多余的字段(比如多传了location但函数没这个参数),直接用**fn_args展开会炸。稳妥做法是取inspect.signature里的参数名做过滤,只保留函数真正需要的键。
5.3 Agent陷入死循环或者无限重试
场景很典型:模型调用工具,工具返回报错,模型看不懂错误,换个参数重试同一工具,又失败,往复循环直到把预算烧穿。
这种问题的解法是“重试要有代价”。我给工具调用加失败状态信息,并且监控连续失败的次数,连续失败达到阈值就强行切换策略。具体做法:
- 工具返回结果要带上清晰的错误信息,告诉模型“参数错了还是服务错了”,模型才能正确决策。
- 历史消息里保留尝试记录,引导模型“不要重复尝试已失败的工具,可以考虑换一种途径或直接告知用户失败”。
- 代码层面加熔断:同一个工具连续失败3次,本次Agent立即终止并返回异常。
经常在调试时看到模型会“一意孤行”,明明工具返回“查无此用户”,它非要换个ID再查一次。设计上要提示它:特定工具失败后尝试别的工具或直接回答,而不是反复调用同一个工具。
5.4 上下文溢出和Token预算超支
Agent跑着跑着上下文字数爆了,这是生产环境常见问题。除了之前说过的摘要压缩和滑动窗口,还有一个小技巧:工具返回结果要瘦身。很多工具返回的原始数据非常大(比如搜索返回30条结果、每条约500字),不要让Agent一次性吞掉。在工具内部先做一次过滤摘要,只返回前5条核心信息,或者把长文档改传链接,让模型按需再查。
Token预算方面,我建议在开跑前就算一笔账:估算每一步的输入输出token量,乘上预估轮数,算出单次任务成本。如果成本超预算,说明链路里某处调用过于频繁,优先优化工具返回内容长度和上下文压缩策略。
5.5 模型输出质量不稳定,时好时坏
这基本是Agent开发到一定阶段必然会遇到的老大难问题。我的经验是:不要追求单模型完美,要接受“概率性失败”,然后用工程手段弥补。
具体来说:
- 多次采样法:对同一轮决策调用模型两次以上,让模型给出多个候选方案,再用一定规则选优。但注意这个方法会让成本翻倍,只建议在关键节点用。
- 约束输出法:如果只是格式问题,比如要求模型输出JSON却经常夹带注释,可以强制规定输出格式,并在代码里容错解析(去掉多余括号、修复截断)。
- 固定最终答案格式:让模型在最终输出时按固定模板组织语言,可以明显提升下游解析的成功率。
最重要的还是“回归测试集”,这是Agent质量的生命线。你改任何一次Prompt、换任何一个模型版本,都必须跑一遍测试集对比前后效果,否则没法持续迭代。
写在最后
Agent开发跟传统的后端开发真的不一样。传统编程是确定性的,同样的输入必然产生同样的输出;Agent是概率性的,同一个Prompt,模型可能走出两条完全不同的路径。你必须在接受“不确定性”的前提下,通过架构设计把不确定性限制在可控范围里。工具要克制、设定要清晰、护栏要到位、回归要常跑。
我个人做了这么多Agent项目,最大的体会就是不要去对抗模型,也别幻想模型无所不能。把脏活累活留给代码,把判断取舍留给模型,两者各司其职,这才能让Agent真正从demo走向生产。如果你是从零开始,我建议你把前面那段最小示例拿到本地,跑通、改乱、再修好,整个过程比看十篇教程都有用。等这个循环吃透了,RAG、多Agent协作、记忆工程那些东西都是水到渠成的事。