大模型Agent开发入门:原理、框架选型与部署避坑指南
最近半年,我几乎每周都被问同一个问题:大模型Agent开发到底从哪入手。问的人有后端转过来的,有做数据分析的,也有产品出身的产品经理,大家的困惑出奇一致——Demo看了一堆,LangChain文档也翻了不少,但真要自己动手搭一个能办事的Agent,又不知道从哪一步开始。这篇文章就按照我自己实际踩坑的路径来写,把Agent开发从概念到代码、从框架选型到部署上线,一条线串清楚。如果你是准备从零开始接触Agent的开发者,照着这条线走,应该能少绕不少弯。
1. Agent的本质:不要用“聊天机器人”来定义它
1.1 从“会说话”到“会做事”:ChatBot和Agent到底差在哪
我面试过不少自称做过Agent开发的候选人,大多数给我的感觉是:把一个带上下文记忆的聊天机器人包装成了Agent。这个词确实被用得太滥了,导致很多人觉得只要能调用一次函数,就算Agent。
我自己对Agent的定义很朴素:能自主决策、调用外部工具、根据反馈修正行动路径的LLM应用。它不再是“生成下个词”,而是“为了完成一个目标不断采取行动”。ChatBot的终点是生成一段合理的文本,Agent的终点是把一件具体的事做完成。
这个区别最直接的体现是出错之后的行为。聊天机器人回答错了,改一下prompt重新生成一次就行;Agent执行出错了,它需要自己判断下一步怎么纠正,必要时换一条完全不同的路径。这背后涉及工具调用、结果解析、错误恢复、记忆维护等一整套围绕模型的系统工程,远不止“调API”这么简单。
1.2 感知-决策-行动-反思:最简Agent闭环
任何Agent,不管宣传册上写得多么玄乎,落到代码层面都离不开四件事:
- 感知(Perception):接收用户请求、读取上下文、获取环境状态。
- 决策(Decision):LLM根据当前状态和可用工具,决定下一步动作。
- 行动(Action):调用外部工具,拿到结构化返回结果。
- 反思(Reflection):把工具结果和任务目标做对比,判断是继续执行还是宣告完成。
这四件事构成一个循环。核心结论是:LLM是这个循环的决策中枢,但不是唯一部件。很多入门者只盯着模型参数、Prompt技巧,却忽略了周围三样东西——工具、记忆、循环控制。这三样才是决定Agent能不能真正落地的关键。
1.3 一个更好理解的类比:你雇了一个远程实习生
把Agent想象成一个只通过文字交流、随叫随到的远程实习生。你布置任务:“把数据表里所有空值替换成列均值,然后生成一份汇总报告。”实习生会怎么做?先问你报表在哪里,找到后打开工具执行操作,发现某列有空值,如果搞不定会回来问你,最后把结果整理给你。
Agent的运行机制几乎就是这套流程。LLM扮演实习生的“大脑”,工具调用就是它的键盘和鼠标,记忆是它的工作笔记,循环控制是你们的沟通节奏。这样想,很多设计决策就清楚了——你不可能给实习生布置任务后直接失联,Agent也是一样,必须有反馈循环和状态同步。
2. 框架的代价:LangChain还是裸写API
2.1 为什么我不建议一上来就上LangChain
现在提到Agent开发,很多人第一反应就是LangChain。但我在做了几个真实项目之后,对这个“标配”有了不太一样的看法。
LangChain的价值在于组件化,模型调用、消息构造、AgentExecutor、检索器这些都能排列组合。但代价也很明显:学习成本高、抽象层级多、排错困难。框架把你和模型原始协议之间隔了好几层,出了问题你根本不知道是工具返回格式的锅,还是框架内部Prompt模板的锅,还是模型理解的锅。我在一个项目里排查了两个小时,最后发现是框架某个内部的缓存逻辑和我业务的定时器冲突。
所以我的建议很直接:先裸写一个最小Agent,再把LangChain搬进项目。裸写的目的不是让你生产环境也不用框架,而是让你彻底看懂每一个环节发生了什么。之后再把LangChain当顺手工具,而不是当黑盒。
2.2 LangChain里真正高频用到的组件
如果要用LangChain,我个人觉得重点看这几个模块就够了:
@tool装饰器或BaseTool:定义工具的执行逻辑和参数Schema,框架会帮你生成工具描述。AgentExecutor:负责跑起“思考-行动-观察”循环,处理模型中断、工具异常这些边界。ConversationBufferMemory和ConversationSummaryMemory:管理对话历史的短期记忆。create_openai_functions_agent:走Function Calling协议的Agent入口,比常见的create_react_agent在结构化输出上要稳得多。
实际上,很多项目用了LangChain全家桶,但业务逻辑里天天碰的也就这几个类。不要为了用框架而用框架,这个在工程上是最容易走偏的地方。
提示:如果你的业务逻辑简单、只需要两三个工具,我自己会直接跳过框架裸写。如果你的项目需要重度编排、大量工具切换、多个模型联合调度,再上LangChain才划算。框架是给团队协作用的,不是给单个脚本用的。
2.3 中间路线:自己写管道,框架当组件用
我目前的偏好既不是纯LangChain,也不是纯裸写,而是“半框架”模式。
核心循环自己写,定时器、异常处理、日志全部自己控制。LangChain的工具封装或者模型客户端可以用,但只当组件调用,不套它的AgentExecutor。这样做的收益是业务逻辑可读性高,线上出问题能顺着自己的代码直接排查,不会被框架几十层抽象挡住。
这个取舍没有绝对对错,取决于团队经验和项目规模。但入门阶段一定要先做一次裸写,你才能真正判断框架什么时候在帮你,什么时候在坑你。
3. 手写一个最小Agent:核心引擎的完整实现
3.1 核心循环:没有框架的Agent长什么样
我写第一个最小Agent的时候,只用了一个while循环加两个函数。下面给一个可以实际跑起来的浓缩版。为了演示,我假设调用的是任意兼容OpenAI协议的服务,工具只做一个get_weather:
import json from openai import OpenAI client = OpenAI(base_url="...", api_key="...") # 1. 定义工具,描述和入参Schema给模型看 tool_spec = { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气,用于回答天气类问题", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市中文名称,例如:北京、上海、广州。不要带'市'后缀。" } }, "required": ["city"] } } } def get_weather(city: str) -> str: # 这里替换成真实天气API return json.dumps({"city": city, "temperature": 22, "unit": "Celsius"}) # 2. 工具路由 TOOLS = {"get_weather": get_weather} def run_agent(user_input: str, max_steps: int = 5): messages = [{"role": "user", "content": user_input}] for step in range(max_steps): resp = client.chat.completions.create( model="deepseek-chat", # 可替换成任意兼容OpenAI协议的服务 messages=messages, tools=[tool_spec], tool_choice="auto" ) msg = resp.choices[0].message messages.append(msg) # 先把模型原始返回放入会话 # 3. 如果模型决定调用工具,执行并回填结果 if msg.tool_calls: for call in msg.tool_calls: fn = call.function.name args = json.loads(call.function.arguments) result = TOOLS[fn](**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result }) continue # 本轮有工具调用,继续循环 # 4. 没有工具调用,说明模型认为可以直接回答了 return msg.content return "达到最大步骤数,任务未完成"这段代码的核心就是两件事:把模型返回的工具调用请求翻译成真实函数执行,再把执行结果作为role=tool的消息回填。max_steps是给整个循环一个硬性出口,防止模型陷入无限的工具调用。别小看这个参数,我见过不加限制的Agent在一个简单查询上死循环了五十多轮。
3.2 闭环比裸调API强在哪里
如果你只用一次API调用,把工具描述塞进system prompt,靠模型在自然语言文本里“说”它要调用工具,然后再从返回文本里用正则抠参数——这个方案我用小模型试过,能跑通但非常脆弱。
问题出在“结构化”和“确定性”上。Function Calling让模型通过原生字段输出tool_calls,而不是在输出文本里“假装调用”,这样你能拿到标准化的函数名和参数JSON,不需要自己去解析长文本。第二轮把role=tool的结果回填后,模型看到的是一个连续且确定的状态切换,而不是夹杂着错误引用的伪代码。
这种确定性的差距在生产环境会被无限放大。一次错误的参数解析,轻则返回错误结果,重则让下游系统拿到错误数据。所以,工具调用的协议化是Agent落地的第一道基础。
3.3 从最小代码到生产代码的三个升级点
上面这段代码只是能让Agent跑起来的骨架。真要上线,我建议先补三样东西:
- 错误捕获:工具执行可能抛异常,捕获后把错误信息返回给模型,让它自己决定要不要换参数重试。
- 防抖机制:模型经常出现“任务已经完成却还在调用工具”的情况。可以按工具统计调用次数,超过阈值就强制终止本轮。
- 结构化输出校验:Agent最终给用户的答案应该走一次Schema校验,关键字段缺失就触发一次重试,避免下游解析报错。
这三样加完之后,原本的Demo代码才算真正有资格进生产环境。
4. 工具调用与记忆:Agent的手脚和记事本
4.1 Function Calling是协议,不是插件机制
这里我想强调一个容易被忽略的认识:Function Calling在Agent生态里更准确的理解是“协议”,而不是“插件系统”。
模型不关心你的函数内部是Python、Java,还是跑在另一个微服务里。它做的只是把“用户意图+工具描述”映射成“该调用哪个函数+参数是什么”。你的工作就是提供一份符合协议的工具描述,然后在收到调用请求时安全执行。这个协议跨语言、跨服务都能用,因为它走的是标准JSON。
所以设计工具时,我基本遵守三条原则:
- 工具名称别太长,但描述必须写清楚用途、边界、参数含义。
- 参数少而准,超过五个参数模型就容易出错。尽量把一个重工具拆成几个独立的小工具。
- 返回值尽量平铺可读,不要返回一个嵌套极深的对象,模型理解复杂JSON的能力没有你想象的那么强。
4.2 JSON Schema写不好,Agent就会“手抖”
工具描述本质是一份JSON Schema。很多第一次写工具描述的人,给参数写了一句模棱两可的说明,模型就不知道该填“北京”还是“北京市”还是“beijing”。我见过最离谱的一次是模型把city参数填成了一整段地址。
正确的做法是给每个参数写上明确示例和约束,相当于教实习生怎么填表。常见错误和正确做法可以这样对比:
| 常见写法 | 模型表现 | 正确做法 |
|---|---|---|
"description": "城市名称" | 填出各种格式 | 写明中文名称、推荐示例、是否带后缀 |
"description": "用户输入" | 经常传错参数 | 明确参数含义和单位 |
| 缺少枚举约束 | 模型自由发挥 | 用enum或format限定取值 |
| 参数过多 | 漏传、错传 | 拆分成多个小工具 |
把模糊边界写清楚,Tool Calling的准确率会立刻上一个台阶。这是我在多个项目里验证过、提升工具调用准确率最廉价的手段。
4.3 三层记忆设计:先别急着上向量库
Agent的“失忆”是入门者最容易忽略的另一个坑。典型的场景是:用户上一步刚告诉Agent自己偏好什么,下一步Agent就忘了,还反问一遍。
记忆至少可以拆成三层:
- 会话级记忆:就是messages里累积的对话历史,模型通过看前面的消息维持上下文。
- 业务级记忆:把用户身份、偏好、常用参数存进业务数据库,下次请求时经检索塞回上下文。
- 向量级记忆:把历史交互、知识片段做embedding存进向量库,用相似度召回相关片段。
我建议从会话级开始,等上下文长度真的成为瓶颈了,再引入业务级或向量级。一上来就搭一套向量数据库,很容易误判问题根源。
一个简单的会话记忆管理器可以是这样的:
from collections import deque class MemoryBuffer: def __init__(self, max_tokens=3000): self.history = deque() self.max_tokens = max_tokens def add(self, message: dict): self.history.append(message) self._trim() def _trim(self): # 粗略按字符数压缩,线上应按token数精确计算 total = sum(len(m.get("content", "")) for m in self.history) while total > self.max_tokens and len(self.history) > 2: self.history.popleft() total = sum(len(m.get("content", "")) for m in self.history) def get_messages(self): return list(self.history)这个管理器会在消息超过阈值时把最早的对话丢出去,保留最近的内容。真实生产环境的做法会更复杂,还要考虑摘要压缩、关键实体抽取,但“最近内容最重要”这个直觉是对的。
5. 规划与多Agent:从单兵作战到小队协作
5.1 ReAct模式:边思考边行动边观察
ReAct是目前单Agent最常用的规划范式,让模型每一步都输出Thought(思考)、Action(动作)、Observation(观察)三个部分。工具结果作为Observation回填,模型再根据观察结果修正下一步。
这个模式最适合路线不明、需要实时调整的任务。但一个关键点是:工具结果一定让模型看得懂。如果Observation是一段五万字的日志,模型根本没法据此做正确决策。我在实现里会把工具输出先截断或摘要,再抛回给模型。
推荐读一下完整的代码结构。实际中这个模式配合max_steps效果很稳,不会让单次任务失控。
5.2 Plan-and-Execute:先规划再执行,适合流程型任务
和ReAct不同,Plan-and-Execute让模型在正式执行前先生成一份完整计划,然后逐步执行。它更适合流程相对固定、后面步骤依赖前面结果的任务,比如“先查询库存,再计算物流费用,最后生成报价单”。
两者不是互斥关系,很多系统会先做Plan生成阶段,再对计划中的每一项走ReAct执行。但这样工程复杂度会成倍增加,入门阶段先跑通一种模式就好。
5.3 多Agent协作:先问自己一个Agent搞不定什么
很多人一看到Agent就想着搞“多个Agent分工协作”,我劝你先冷静一下。多Agent的直接代价是:上下文翻倍、工具调用链变长、错误率指数上升。如果你单Agent跑通一个任务都费劲,多Agent只会更糟。
只有当任务确实存在天然边界,才可以考虑多Agent。比如“导演Agent负责拆任务,执行Agent负责调用工具,质检Agent负责校验”,它们之间用消息队列或共享状态通信。
我踩过多Agent最大的坑:最初让两个Agent共享同一个对话历史,结果A Agent的思考过程被B Agent当成用户指令,任务执行彻底跑偏。后来改成每个Agent维护一套自己的消息记录,只把关键结果传给对方,问题才消失。共享状态看起来简单,到了并发真实场景,谁先写谁后写、脏读乱序这类问题会把你逼疯。
6. 部署现场:响应速度、并发和成本的三角博弈
6.1 本地快线上慢的真相:一次Agent任务等于多次模型请求
接手Agent项目后最直观的感受就是:本地测试200毫秒,上了线变5秒。原因很简单,本地你只发一两次请求,而且多半用的是轻量模型或者有缓存;线上是真实模型服务,要排队、要流式传输、要等工具调用来回多轮。一次带两个工具调用的Agent任务,往往意味着三到四次模型请求,总时长冲到十秒并不稀奇。
优化手段有三个方向:
- 把Agent循环里的模型请求改成流式输出,用户至少能看到“正在输入”的反馈。
- 对工具结果做缓存,比如同一个城市的天气查询,十分钟内直接命中缓存。
- 把重上下文从“每次全量发送”改成“检索关键片段拼装消息”。
这三条做完,响应速度通常能改善一个量级。尤其是缓存,在工具返回值变化不频繁的场景下效果好得惊人。
6.2 并发和成本:Agent比普通ChatBot贵得多
Agent成本问题在入门阶段容易忽略。传统ChatBot一问一答是一次模型请求,Agent一个任务可能发起多次请求,成本直接翻几倍。再加上模型决策的不确定性,上线前需要大量测试迭代,费用会非常可观。
我的做法是:按任务类型区分模型档位。简单工具调用用轻量模型,复杂推理才切到大模型;同时用max_steps限制行动步数,防止模型在同一个任务上反复横跳。这些措施看起来像在“克扣”Agent的能力,但实际做下来效果没有明显变差,反而更可控、更省钱。
6.3 上线前必须有的三件套:日志、评测、回归
Agent开发和传统接口开发最大的区别在于结果不确定性。同一个用户输入,今天走这个路径,明天可能走另一个路径。上线前我会强制自己补齐三样东西:
- 全量日志:把每次任务的消息历史、工具调用、模型返回全部落库。
- 评测脚本:准备一组成熟case,每次改完代码自动跑一遍,看通过率。
- 回归对比:保留上一版输出结果,用来对比当前版本有没有变蠢。
没有这三件套之前,Agent项目线上出了问题你只能干瞪眼。补齐之后,你至少能快速判断是模型的问题、工具的问题还是上下文构建的问题。
7. 入门最容易踩的坑:给赶路的人提个醒
7.1 Prompt写得太“礼貌”,Agent反而变笨
我见过很多人把system prompt写成一封客气到极致的邀请函:“作为一名优秀的AI助手,您能够……请您根据用户的需求,选择适当的工具进行调用。”这种写法让模型花了一堆token理解客套话,真正的指令反而被稀释了。
我的原则是:prompt里的每一句都要服务决策。把目标写明确、边界写清楚、出错怎么办写明白,比任何修饰都有用。系统提示里最有价值的部分永远是“你是做什么的、你可以做什么、你不可以做什么”。
7.2 工具不是越多越好
给模型注册二十个工具,它反而不知道该选哪个。工具越多,模型在工具选择上的出错率越高。一般项目五到八个工具是一个舒适区。
如果确实需要大量工具,一个有效办法是“工具分组”:先让模型经过一个分类步骤,确定自己需要哪一组工具,再去那组工具里做选择。这相当于给Agent加了路由层,能显著降低工具选择的困难。
7.3 别把开源Demo代码直接搬进生产
开源社区的Agent项目更新非常频繁,很多Demo能跑通依赖特定版本、特定模型甚至特定模型的回答风格。换一个模型、改一个参数,整个链路的准确率可能直接掉一半。
我把这类代码当“起点的参照物”,而不是“生产的代码库”。要真正用到业务里,至少要自己过一遍错误处理、超时控制、日志埋点这些工程化的细节。Demo证明的是“路径可行”,不等于“代码可靠”。
最后聊一下我自己的感受。入行做Agent开发这半年多,最大的体会是:让你上头的不是某个模型今天又有多聪明,而是你亲手把一个“只会聊天”的系统,变成了一个能自己查数据、能自己调工具、能自己把一整个流程跑完的闭环。这个过程的难点从来不在模型选型,而在工程化——工具、记忆、循环、评测、日志,每一环出错,整体效果都会大打折扣。希望这篇入门指南能帮你少踩一些我踩过的坑,期待看到你的第一个Agent真正跑起来。