说实话,这两年“大模型Agent”这个概念的热度一直没降过。每刷几个技术社区,就能看到有人问“Agent到底是什么”“这玩意儿怎么落地”,也有人直接上手折腾,说翻车翻得厉害。我自己是从去年年初开始接触Agent开发的,从最早跟着教程写一个能答题的脚本,到后来尝试做能调工具、能查资料、能多步完成任务的智能体,踩了不少坑,也总结了不少经验和教训——这篇就当作我对自己这段弯路的一次系统复盘,顺便把大家比较容易卡住的地方讲明白,希望能帮你把“入门”这件事真正落地成“能跑起来”。
这篇内容主要是写给有Python基础、但对大模型和Agent不熟悉的开发者的。如果你之前只写过脚本、没碰过LLM的API,也没关系,我会从最基础的“Agent到底是什么”讲起,一步步拆到工具调用、记忆管理、任务循环这类核心机制,再讲框架选型和工程化避坑。全文的落脚点是“怎么自己动手写出一个最小可用Agent”,而不是只讲概念。
1. 先理清思路:大模型Agent到底是什么
1.1 别再把它当成一个Chatbot
很多人一提到大模型Agent,第一反应就是“这不就是个聊天机器人吗”。这句话对了一半,但只对了一半。普通的Chatbot是你问一轮答一轮,即使上下文再长,它本质上还是“语言模型在做文本续写”。而Agent的关键差异在于:它有“手”,可以操作外部工具,有“目标”,可以自主拆解任务,有“记忆”,可以跨多轮对话保留有效信息。
我习惯用一个比喻来理解:大模型本身像一个“只会读书和写方案的实习生”,知识面很广,但不能动手执行任何实际操作。Agent是这样做的——你给这个实习生配了一双手(工具调用),给了一个工作台(代码执行环境或API),给了一本工作日志(记忆),再定了一套工作流程(规划与循环)。大模型负责想,Agent负责把“想”变成“做”。
所以你判断自己写的是不是一个Agent,最简单的方式是看一个标准:它有没有一个“循环”。用户发一个目标过来,Agent自己思考要分成几步、每步需要什么资源、该调什么工具,然后调完工具拿到反馈,再判断下一步做什么,直到任务完成或达到终止条件。这个过程在业界被称为“Agent Loop”。没有这个循环的,就只能叫“带上下文的问答接口”。
1.2 Agent的四个核心组件
如果拆开来看,一个能跑起来的大模型Agent通常由四部分组成:
- 大模型底座:负责推理、理解、生成内容。它决定了Agent的“智力上限”。
- 规划能力:将复杂目标拆解成子任务,决定下一步动作。这是“动脑”的部分,常见做法的ReAct模式,后面会展开。
- 工具集合:让Agent能连接外部世界,例如搜索引擎、数据库、计算器、代码解释器、业务系统的API。
- 记忆系统:记录用户偏好、历史对话、中间结果,分为短期记忆(当前任务上下文)和长期记忆(跨会话的知识沉淀)。
这四块里面,最容易动手的是“工具集合”和“规划循环”,因为它们是工程化的部分,有明确写法;最考验能力的是“大模型底座”和“记忆系统”,因为它们的配置和策略直接影响Agent会不会“变傻”。在实际项目里,我见过的很多Agent跑飞,基本不是模型不行,而是规划和记忆这层没做好。
1.3 三种开发路径怎么选
面对“开发Agent”这个问题,市面上有几种路径,刚入门时很容易眼花缭乱:
- 第一种,用成熟框架:比如LangChain、LlamaIndex、AutoGen,框架把循环、记忆、工具调用都封装好了,你主要写工具和Prompt。
- 第二种,用低代码平台:比如Coze、Dify这类可视化编排平台,适合业务快速落地,几乎不用写代码。
- 第三种,完全自研:自己写循环,直接跟模型API交互,用Function Calling机制来触发工具。
我个人的建议是:入门阶段至少花时间把第三种做一遍,哪怕最后不用上线,也要亲手实现一次“大模型返回结构化工具指令→代码执行工具→结果喂回模型”的最小闭环。原因很简单——第三种的每一步链路你都亲眼见过,之后再去看LangChain的源码,会发现很多东西豁然开朗,不觉得那是黑盒了。这篇文章的核心,就是带着你把这个最小闭环完整走一遍。
2. 环境准备与最小开发框架
2.1 模型API怎么选
做Agent开发,不需要上来就自己部署大模型,成本太高,也没必要。直接用大模型API是最快的方式。关键是选哪家。
从开发的友好程度来看,首选支持OpenAI兼容接口的模型服务。你只需要用一个openai的Python SDK,把base_url指到对应的服务地址就行,切换供应商的成本极低。国内能用到的很多模型服务都兼容这个协议,比如智谱、百炼、DeepSeek等——不同厂商的具体名称我这里不一一列举,你去各自官方文档找“OpenAI兼容接口”就能看到。
如果你本机有显卡,想完全本地跑,那可以考虑Ollama配合开源模型。这个方案的优点是没有接口费用,而且离线可用;缺点是模型能力相对弱一些,做复杂的工具调用和长程规划时容易“脑子不够用”。我自己的经验是:入门练手用在线API,跑通流程后,再去尝试本地模型,你会发现很多问题其实是模型能力导致的,而不是你的代码写错了。
2.2 建立一个基础请求链路
不管用哪家API,我们先建立一个最小的调用链路。假设你已经拿到了API Key,并安装了openai库:
from openai import OpenAI client = OpenAI( api_key="your-api-key", base_url="https://your-model-service.com/v1" ) response = client.chat.completions.create( model="your-model-name", messages=[ {"role": "system", "content": "你是一个能调用工具的智能助手。"}, {"role": "user", "content": "帮我查一下今天北京天气?"} ] ) print(response.choices[0].message.content)这段代码能跑通,说明你的API链路没问题。但要注意,这只是“聊天”,还不是Agent。Agent和这个脚本的区别,在于模型返回的不是直接给你看的“答案”,而可能是一个“需要执行什么工具”的指令。这就进入了Function Calling的领地。
2.3 理解Function Calling是Agent的命脉
Function Calling(函数调用)是大模型Agent开发里最核心的机制。说白了,它就是让模型在对话过程中,不只是输出自然语言,还可以输出一个结构化的“调用请求”,告诉系统“你现在需要调用哪个工具,参数是什么”。
举个例子:你在系统里定义了一个工具get_weather(city)。当用户问“北京的天气”时,模型内部可能并不会直接说出答案,而是输出这样一段JSON:
{ "name": "get_weather", "arguments": "{\"city\": \"北京\"}" }你的代码收到这段JSON后,实际执行get_weather("北京"),拿到真实数据,再把这个数据作为一条工具消息返回给模型,模型最后整合成自然语言答复用户。
关键点来了:Function Calling的目标不是让模型真的去“调函数”,而是让模型学会在合适的时候“表达调用函数的意图”,并生成合法的参数。真正执行函数的是你的代码。所以你要做的核心事情有两件:一是把“工具清单”用模型能理解的格式描述出来,二是正确解析模型返回的调用指令并执行。这两件事做扎实了,Agent就是水到渠成的事。
3. 从零写一个最小可用Agent
3.1 先定义好你的工具
假设我们要写一个能查天气、算数学的小Agent。这里我们定义两个极简工具,用来演示完整链路。
import json import random def get_weather(city: str) -> str: """模拟查询天气,返回城市和天气情况""" weather_list = ["晴朗", "多云", "小雨", "大风"] weather = random.choice(weather_list) return json.dumps({"city": city, "weather": weather, "temperature": random.randint(5, 30)}) def calculator(expression: str) -> str: """计算表达式,例如 '1 + 2 * 3'""" try: result = eval(expression) return str(result) except Exception as e: return f"计算失败: {str(e)}"这两段函数的逻辑本身很简单,但在Agent体系里,“函数实现”只是其中一半,另一半是“函数描述”。模型不读你的Python源码,只读你传给它的工具描述。描述写得不好,模型就不知道该在什么时候调用、该怎么传参。
所以,要像下面这样给模型描述工具:
tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如:北京、上海" } }, "required": ["city"] } } }, { "type": "function", "function": { "name": "calculator", "description": "执行数学计算,支持四则运算", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "数学表达式,例如 '3 + 4 * 2'" } }, "required": ["expression"] } } } ]这里有个容易忽略的细节:description写得好不好,直接影响模型调用工具的准确率。比如“查询指定城市的实时天气情况”和“城市天气工具”的差距,在复杂场景下会被明显放大。写描述的时候,把自己想象成一个只会按说明书操作的新员工,说明越明确,出错的概率越低。
3.2 核心循环怎么写
工具定义好之后,重头戏来了。我们希望Agent能自主决定“要不要调工具”“调哪个”“调完再怎么办”,这需要一个循环逻辑。我直接给一个最小可用的循环代码:
# 假设已有 client、messages、tools def run_agent(question): messages = [ {"role": "system", "content": "你是一个智能助手,如果需要工具,请使用工具获取真实信息。"}, {"role": "user", "content": question} ] max_steps = 5 # 防止死循环 for step in range(max_steps): response = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools, tool_choice="auto" ) message = response.choices[0].message # 判断模型是否要求调用工具 if message.tool_calls: messages.append({ "role": "assistant", "content": message.content, "tool_calls": [ { "id": tool_call.id, "type": "function", "function": { "name": tool_call.function.name, "arguments": tool_call.function.arguments } } for tool_call in message.tool_calls ] }) # 逐个执行工具调用 for tool_call in message.tool_calls: tool_name = tool_call.function.name args = json.loads(tool_call.function.arguments) if tool_name == "get_weather": result = get_weather(**args) elif tool_name == "calculator": result = calculator(**args) else: result = f"未知工具: {tool_name}" # 把工具执行结果追加到消息里 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) else: # 模型不再调用工具,返回最终答案 return message.content return "达到最大执行步数,任务结束。" print(run_agent("北京现在什么天气?顺便算一下 15 * 8 等于多少"))这段代码虽然简单,但它已经完整包含了一个Agent循环的关键元素:模型生成工具指令、代码执行工具、把执行结果返回模型、模型继续决策、直到输出最终结果。建议你第一次写完就跑一下,观察消息列表的变化,你会清楚地看到“工具调用不是模型在执行,而是你的代码在执行”这个过程。
有个细节值得留意:tool_choice的参数可以设置成"auto"、"none"或者"required"。"auto"表示让模型自主判断要不要调工具;"required"强制模型这次必须调工具;"none"禁止调工具。调试的时候,如果发现模型明明该调用工具却不调用,可以先强制设为"required"看参数是否正确,再改回"auto"排查Prompt影响。
3.3 记忆管理:Agent有没有“短期记忆”的问题
很多初学者把Agent跑通之后,第一个感觉是“它怎么忘了刚才干过啥”。这就是上下文管理的问题。大模型的API是无状态的,每次请求都要把完整对话历史传给模型。所以记忆管理的本质是:你要在messages数组中维护好“该给模型看什么”。
最简单的记忆就是滑动窗口:
MAX_HISTORY = 10 def trim_messages(messages): if len(messages) > MAX_HISTORY: # 保留 system 消息,去掉最旧的历史 system_msg = messages[0] history = messages[1:] return [system_msg] + history[-MAX_HISTORY:] return messages这种方式实现快,但缺点也很明显:一旦超出窗口,早期信息就被丢光了,Agent对用户说过的话“失忆”。进阶的办法是摘要记忆——每次对话超过一定长度,让模型把历史内容压缩成一段摘要,当作一条消息放在前面。更复杂的是向量记忆,把历史转成embedding存进向量库,关键时刻检索相关的记忆片段进来。入门阶段,我建议先不要折腾向量库,先用“滑动窗口+摘要”组合,大多数日常场景已经够用。
踢一个我在实际项目里踩过的坑:工具执行结果也会占据大量上下文。如果工具返回了一大段JSON,你原封不动塞回messages,几轮对话之后上下文就爆了。正确的做法是在工具返回之前,先做一次裁剪或摘要,只留关键信息。比如查天气的接口返回了几十KB的原始字段,你在返回给模型之前就要格式化成一个简短文本。
3.4 从单一工具到多步任务
前面那个例子虽然能解决“查天气加算数”,但它还不是真正的多步任务规划。在实际场景里,用户的目标通常更复杂,比如“帮我调研一下最新的AI论文,并总结出5个方向”。这种任务需要Agent自己规划:搜索→获取内容→阅读摘要→总结。
一种常见实现方法是让模型在回答过程中使用一个“plan”字段,先列出步骤,再逐步执行。另一种是依赖模型自己在对话中按ReAct方式推进——每一步都思考“当前状态是什么、下一步该做什么”,然后调用对应工具,观察完结果再继续。模型通过System Prompt的引导来完成这种“思考-行动-观察”的循环,所以System Prompt很多时候比你的工具代码还要命。
我见过不少人在这个阶段过早引入复杂的“任务分解框架”。实践下来,如果你的工具层做得足够简洁、Prompt引导清晰,即使不引入额外框架,模型也能比较好地完成串行多步任务。先把串行跑稳,再考虑并行调用多个工具的场景,这样进阶路径会更平缓。
4. 工程化落地:从能跑变成能上线
4.1 上下文危机与Token控制实用策略
我接手过不少刚开始做Agent的团队,第一个线上事故基本都长一个样:用户聊了几轮后,调用模型接口直接报“上下文过长”错误。为什么?因为每轮对话你都在无脑追加消息,LLM的上下文窗口是有限的,而且越接近上限,响应时间越长、成本越贵、效果越差。
在实际开发中,我习惯用这套组合策略来管理上下文:
- 按消息角色分层:system提示语尽量精简;工具返回结果做截断,比如只保留前200个字符或关键字段;用户历史太久远(比如超过20轮)就清掉。
- 维护一个“核心事实”区:不管历史怎么裁剪,用户偏好的关键信息(比如“我叫张小明”“我要简洁的回答”)要单独提取出来,持久化保存,每轮请求都放在system里。
- 将大工具结果降维:工具返回的是结构化JSON时,先抽字段,再转自然语言短句,最后才喂给模型。
Token的计算也不难估,中文大概一个字约1到2个token,英文一个词约1到2个token。你可以先打开官方Tokenizer工具实测一下,心里有个数。我一般给自己定的规则是:输入控制在上下文窗口的三分之二以内,剩余空间留给答案生成和工具调用参数。
4.2 工具调用失败的兜底链路
工具调用真的不是每次都成功的。我在生产环境见过最多的问题是模型生成了错误参数、工具超时、工具直接报错。初学者最常犯的错是“工具挂了,Agent就死机”,这是最典型的工程化缺陷。
正确的做法是做一套兜底链路:
第一层,参数校验:在你执行工具之前,先校验模型生成的参数是否合法,缺了就补默认值,类型不对就转一下。第二层,执行失败反馈:工具抛异常时,不要把错误信息吞掉,要把异常构造一个文本结果返回给模型,让模型根据错误调整,比如改成调用其他工具或换一种说法。第三层,重试机制:一些临时性错误(超时、限流)可以自动重试两三次,但要有上限。第四层,人工兜底:如果Agent连续几次都失败,或者模型自己说“我做不到”,那就直接返回“抱歉,暂时无法处理,已转人工”。
这里补一个我总结过的循环终止条件,在你自己的Agent里务必写清楚:达到最大步骤数;模型不再发起工具调用;工具执行返回明确错误且重试无效;模型输出表示任务结束。少一个条件,你的Agent就可能在某个场景里无限循环烧钱。
4.3 并发、成本与简单评测
聊到上线,绕不开并发和成本。几个核心点:
- 调用频率限制:模型API基本都有QPS限制,尤其是免费档接口。最简单的方案是在Agent内部加一个轻量级的请求队列或令牌桶,控制每秒请求数。
- 缓存:相同或相似的问题不要重复调用模型。尤其是工具返回结果,如果数据源变化不频繁,完全可以在系统里缓存一份,命中就直接返回,成本立省。
- 批量与异步:如果要做并发,先想清楚你的业务是不是真的有并发需求。小团队入门阶段用线程池就够了:
from concurrent.futures import ThreadPoolExecutor with ThreadPoolExecutor(max_workers=4) as executor: results = list(executor.map(run_agent, questions))最后说评测。Agent跟传统程序不一样,同样的输入可能每次输出都不一样。所以我在维护Agent的时候,会固定一套评测问题集,里面覆盖核心工具调用、多轮记忆、拒绝策略等场景。每次改动Prompt或工具定义,就把这套问题集跑一遍,观察正确率有没有下降。这是工程化阶段性价比最高的“体检方案”。
5. 高频踩坑与实战排查
5.1 模型就是不调用工具,怎么排查
如果你发现模型面对明明应该调用工具的问题,却输出一堆文字而不是发起调用,请按顺序排查这四个方面:工具描述是否清晰、模型本身是否支持Function Calling、temperature是否设置过高(建议先调到0减少随机性)、example是否缺失。很多模型在不确定的场景不会主动调工具,你可以在System Prompt里直接写“当用户询问天气时,你必须调用get_weather工具”,立竿见影。
5.2 上下文越长,Agent反而越蠢
这几乎是所有人都会经历的阶段。把完整的历史全都塞进去,模型反而忘了更早的关键信息,或者被一堆无关内容干扰。解决方向是“少而精”:与其保留20条消息,不如把关键信息抽取出来,拼成一段简报。工具调用历史也要清理,中间步骤留下最终结论就好。核心原则是:上下文不是“越多越好”,而是“越相关越好”。
5.3 Agent产生幻觉与越权调用
幻觉问题在非大模型领域也有,但因为Agent会真实调用工具,危险系数更高。比如模型把参数填错、调用了一个不该调用的函数。工程上要做的安全边界有:工具白名单(只允许调用预先定义好的工具);参数白名单或枚举校验(比如城市名、ID范围);敏感词拦截(在工具执行前做一次校验);权限分级(不同的用户Token能看到不同工具集);日志全审计(谁在什么时候调了哪个工具,全部留痕)。
想清楚一个原则:模型只能被允许做“即使做错也不会出大乱子”的事,越权操作必须从系统层面拦截,不能指望模型永远正确。
5.4 常用问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型不调用工具 | Prompt未约束、模型不支持 | 检查模型文档,检查工具描述,强制tool_choice试试 |
| 工具参数总是错 | 描述不全、参数结构过于复杂 | 简化参数结构,给枚举值和默认值 |
| 上下文很快爆掉 | 工具结果未裁剪、历史未清理 | 给工具返回加截断逻辑,启用滑动窗口 |
| Agent陷入死循环 | 缺少终止条件、工具反复失败 | 硬编码最大循环次数,异常时返回最终兜底 |
| 回答质量不稳定 | temperature偏高、模型不适合该任务 | 调低temperature,换更强模型 |
| 成本快速上涨 | 缓存缺失、每轮都全量传历史 | 加缓存,做Token预算限制 |
6. 写在最后的实操心得
按我个人的经验,大模型Agent开发入门,最忌讳的就是一上来就追框架、追新概念。框架能帮你节省时间,但也容易让你迷失在抽象层里面。我自己更推荐“先徒手造轮子,再造熟了再引入轮子”这种顺序:写一个不带框架的最小Agent循环,跑通,亲手观察每一步消息是长什么样的、工具结果是怎么回流的。等你理解了这一整条链路,再回头去用LangChain之类的框架,会发现自己不再怕它——因为你已经知道底层在做什么了。
还有一个小技巧分享给你:调试Agent的时候,别只看最后输出的答案,一定要把每轮循环的messages、tool_calls、工具返回都打印出来,甚至写成一个可视化的日志面板。我见过太多人因为“看不到过程”,只能在结果里盲目调Prompt,效率极低。把过程可视化之后,很多问题一眼就能定位——是模型判断错了,还是工具返回有问题,还是历史丢了。把这条基本功练扎实,你的Agent开发之路会顺畅很多。