1. AI Agent 开发到底在开发什么
先把一个最常见的误区说清楚:很多人把 AI Agent 开发和“调用大模型 API”画等号,觉得写个函数把问题丢给模型、拿回答案就算做完了。这顶多叫“套壳聊天”,离 Agent 还差得远。Agent 的核心在于自主性——它能自己拆解目标、自己决定下一步调什么工具、自己判断结果对不对、错了还能自己绕回来。你写的不是一段问答逻辑,而是一套让模型在受控范围内“自己干活”的运行机制。
那 Agent 和 LLM、AI 模型到底是什么关系?我习惯用一句话概括:LLM 是大脑,Agent 是给这个大脑装上手脚、记忆和纪律的完整系统。DeepSeek、GPT 这类属于底层模型,负责理解和生成;Agent 则是在模型外面包了一层循环控制、工具调用、状态管理和错误处理。你问“DeepSeek 属于哪个”,它属于模型层,是 Agent 可以选用的“大脑”之一,但它本身不是 Agent。同理,LangChain、LangChain4j 这类属于框架层,帮你把循环和工具调用标准化,也不是 Agent 本身。
这套东西适合谁?我的判断是三类人值得投入:一是已经有后端或前端开发经验、想往 AI 应用方向转的工程师;二是手里有具体业务场景(比如自动化运维、数据处理、客服分流)想用 Agent 落地的技术负责人;三是想系统入门、不想只停留在“调 API”层面的学习者。如果你连基本的 HTTP 请求、JSON 解析、异步编程都不熟,建议先把这些补上再来,否则会在调试环节被绕晕。
我见过太多人一上来就冲着“多智能体协作”“自主规划”这些大词去,结果连单 Agent 的工具调用都没跑通。所以这篇我按真实开发顺序来拆:先讲整体设计思路,再讲核心结构,然后是完整实操,最后是我踩过的坑和排查方法。你跟着走一遍,至少能搭出一个能稳定跑起来、能扩展的 Agent 骨架。
2. 整体设计与架构思路拆解
2.1 为什么 Agent 必须是一个循环而不是一次调用
普通 API 调用是线性的:输入进、输出出,结束。Agent 不行,因为它的任务往往需要多步。比如你让它“查一下这个季度销售额,和上季度对比,生成一份简报”,它得先调数据库查询工具,拿到数据,再调计算工具做对比,最后调文本生成。中间任何一步失败,它都得知道并调整。
所以 Agent 的骨架一定是一个带终止条件的循环:模型思考 → 决定动作 → 执行动作 → 观察结果 → 再思考,直到模型认为任务完成或触发最大轮次限制。这个循环在业界常被称为 ReAct 模式(Reasoning + Acting),是目前最主流也最实用的范式。我实测下来,90% 的业务场景用 ReAct 就够了,不需要上更复杂的规划算法。
这里有个关键设计决策:循环的终止权交给谁。我的做法是双保险——模型可以主动输出“完成”信号,同时代码层强制设置最大迭代次数(一般 8 到 15 轮)。只靠模型自己判断,它有时候会陷入死循环反复调同一个工具;只靠代码强制,又可能任务没做完就被切断。两者结合最稳。
2.2 工具调用是 Agent 的手脚,设计好坏决定成败
Agent 能不能干活,全看工具设计。我见过最典型的新手错误是把工具做得太“大”——一个工具干十件事,参数一大堆。结果模型根本不知道该传什么,调用失败率极高。正确做法是一个工具只做一件明确的事,参数尽量少且语义清晰。
举个例子,你要做数据查询,不要设计一个query_data(type, table, condition, fields, limit, order)这种万能工具,而是拆成get_sales_by_quarter(quarter)这种语义单一的工具。模型看到工具名和描述就能明白什么时候用,参数也容易填对。工具的描述文字(description)尤其重要,它是模型选择工具的唯一依据,必须写清楚“这个工具做什么、什么时候用、参数含义”。
工具的数量也要控制。我个人的经验是单 Agent 挂 5 到 10 个工具比较合适,超过 15 个模型的选择准确率会明显下降。如果业务确实需要很多能力,就考虑拆成多个 Agent 或者做工具分组。
2.3 记忆和状态管理:别让 Agent 变成金鱼
Agent 如果没有记忆,每一轮都像失忆一样重新开始,任务根本没法推进。记忆分两层:短期记忆是当前任务的对话历史和中间结果,长期记忆是跨会话的知识沉淀。短期记忆相对简单,把每轮的思考、动作、观察结果按顺序存进上下文就行。长期记忆复杂得多,通常需要向量数据库做检索。
这里有个成本陷阱:上下文不是越长越好。每轮都把全部历史塞给模型,token 消耗会爆炸,而且模型在超长上下文里反而容易抓不住重点。我的做法是滑动窗口加摘要——保留最近 N 轮完整记录,更早的内容压缩成一段摘要。N 一般取 5 到 8 轮,实测能覆盖大多数任务的推理需求。
至于 MCP(Model Context Protocol)这类协议,它的价值在于把工具和资源的接入标准化,让不同 Agent 能复用同一套工具定义。如果你只是做单个项目,不一定非要上 MCP;但如果要构建多个 Agent 共享工具生态,它确实能省很多重复工作。
2.4 框架选型:LangChain4j、原生 SDK 还是自己写
框架选型是绕不开的问题。我的建议分情况:
- 快速验证想法:用 LangChain 或 LangChain4j,它们把循环、工具调用、记忆都封装好了,几十行就能跑起来。Java 技术栈的话 LangChain4j 是首选,文档虽然不算特别全,但核心功能够用。
- 生产环境、要求可控:我倾向于自己写循环,只借用框架的工具定义和模型调用部分。因为框架的抽象层在出问题时很难调试,你不知道到底是模型的问题还是框架封装的问题。
- 学习目的:强烈建议先手写一遍最简循环,哪怕只有几十行。理解了底层机制,再用框架就是如虎添翼,而不是被框架牵着走。
我自己的项目现在是混合模式:模型调用和工具 schema 用框架的,主循环和错误处理自己写。这样既有开发效率,又保留了调试的透明度。
3. 核心结构解析与实操要点
3.1 Agent 的四大组成部件
一个能跑的 Agent,拆开来看就四块:模型、工具、记忆、循环控制器。模型负责推理和决策,工具负责与外部世界交互,记忆负责保存状态,循环控制器负责调度和终止判断。这四块缺一不可,而且每一块都有坑。
模型这块,选型要考虑三点:推理能力、工具调用能力(function calling)、成本和延迟。不是越大的模型越好,很多任务用小模型加好的提示词就能搞定,成本能降一个数量级。工具调用能力是硬指标,有些模型虽然对话很强,但 function calling 格式老出错,这种就不适合做 Agent 的主脑。
工具这块,前面说了要单一职责。补充一个细节:工具的返回值格式要稳定。我习惯统一返回 JSON,包含success、data、error三个字段。这样模型看到结果就知道成功没有,失败了也知道原因,能做出正确反应。如果工具返回格式五花八门,模型很容易误判。
记忆这块,短期记忆用列表存就行,长期记忆才需要向量库。循环控制器是很多人忽略的部分,它要处理超时、最大轮次、异常中断这些边界情况。
3.2 提示词工程:Agent 的“岗位说明书”
Agent 的系统提示词(system prompt)比普通对话重要得多,因为它要定义 Agent 的角色、能力边界、行为规范。我写系统提示词一般包含这几块:
- 角色定义:你是谁,负责什么。
- 可用工具说明:虽然工具 schema 会单独传,但提示词里再强调一遍使用场景有帮助。
- 行为规范:比如“每次调用工具前先说明你的意图”“如果工具返回错误,尝试换一种方式或告知用户”。
- 输出格式:明确要求模型按什么结构输出思考和动作。
这里有个实操心得:提示词里的行为规范要具体,不要抽象。写“请谨慎使用工具”没用,写“同一个工具连续调用失败两次后,停止调用并汇报问题”才有用。模型对具体指令的遵循度远高于模糊要求。
3.3 工具调用的参数校验与容错
模型生成的工具参数经常有问题:类型不对、缺字段、值超出范围。如果你直接把参数传给工具函数,轻则报错,重则产生副作用(比如删错数据)。所以参数校验是必须的。
我的做法是在工具执行前加一层校验:检查必填字段、检查类型、检查取值范围。校验失败不直接抛异常,而是把错误信息作为观察结果返回给模型,让它自己修正。这样 Agent 就有了自我纠错能力。实测下来,加了这层校验后,任务成功率能提升不少,因为模型看到“参数 quarter 必须是 1 到 4 的整数”这种明确反馈,下一轮基本能改对。
容错还包括超时处理。工具调用可能卡住,必须设超时。超时后返回一个明确的错误结果,让循环继续,而不是整个 Agent 挂死。
3.4 上下文管理与 token 成本控制
token 成本是 Agent 落地时绕不开的现实问题。一个多轮任务,每轮都要把系统提示词、工具定义、历史记录全部传给模型,消耗是累加的。我算过一笔账:一个 10 轮的任务,如果每轮平均 3000 token 输入,总共就是 3 万 token,用大模型的话单次任务成本可能到几毛钱。量大了就是真金白银。
控制成本的手段有几个:一是精简系统提示词和工具描述,去掉冗余表述;二是滑动窗口,只保留最近几轮;三是对历史做摘要,把早期轮次压缩;四是分级用模型,简单决策用小模型,复杂推理才用大模型。我现在的项目就是混合调用,成本比全用大模型降了大概六成。
4. 完整实操:从零搭一个能跑的 Agent
4.1 环境准备与依赖安装
我用 Python 举例,Java 技术栈的把对应库换成 LangChain4j 即可,逻辑完全一样。基础依赖就几个:模型 SDK、HTTP 库、以及可选的向量库。
pip install openai requests numpy如果你用 LangChain 生态,再加:
pip install langchain langchain-community环境变量里配好模型 API 的地址和密钥。我习惯用.env文件管理,代码里用os.getenv读取,避免密钥硬编码进代码。
4.2 定义工具函数与 schema
先定义两个简单工具做演示:一个查天气,一个算数。真实项目里换成你的业务工具。
import json def get_weather(city: str) -> dict: # 实际项目里这里调真实天气 API mock_data = {"北京": "晴 25度", "上海": "多云 28度"} if city in mock_data: return {"success": True, "data": mock_data[city], "error": None} return {"success": False, "data": None, "error": f"未找到城市 {city} 的天气"} def calculate(expression: str) -> dict: try: # 生产环境务必用安全的表达式解析,不要直接 eval result = eval(expression, {"__builtins__": {}}, {}) return {"success": True, "data": result, "error": None} except Exception as e: return {"success": False, "data": None, "error": str(e)}对应的工具 schema,这是给模型看的:
tools_schema = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气。当用户询问天气时使用。", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,如 北京"} }, "required": ["city"] } } }, { "type": "function", "function": { "name": "calculate", "description": "计算数学表达式。当需要做算术运算时使用。", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式,如 3*5+2"} }, "required": ["expression"] } } } ]注意description我写得比较具体,明确说了“什么时候用”,这是提高模型选择准确率的关键。
4.3 主循环实现
这是 Agent 的心脏。核心逻辑就是不断问模型、执行工具、把结果喂回去,直到模型不再要求调工具。
import os from openai import OpenAI client = OpenAI(api_key=os.getenv("API_KEY"), base_url=os.getenv("BASE_URL")) TOOL_MAP = {"get_weather": get_weather, "calculate": calculate} def run_agent(user_input: str, max_turns: int = 10): messages = [ {"role": "system", "content": "你是一个助手,可以使用工具完成任务。每次调用工具前先说明意图。任务完成后直接给出最终答案。"}, {"role": "user", "content": user_input} ] for turn in range(max_turns): response = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools_schema, tool_choice="auto" ) msg = response.choices[0].message messages.append(msg) # 没有工具调用,说明任务结束 if not msg.tool_calls: return msg.content # 执行每个工具调用 for tool_call in msg.tool_calls: name = tool_call.function.name try: args = json.loads(tool_call.function.arguments) except json.JSONDecodeError: result = {"success": False, "data": None, "error": "参数不是合法 JSON"} else: if name in TOOL_MAP: result = TOOL_MAP[name](**args) else: result = {"success": False, "data": None, "error": f"未知工具 {name}"} messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) return "达到最大轮次限制,任务未完成"这段代码就是最小可用的 Agent。你可以直接跑,输入“北京天气怎么样,顺便算一下 25 乘以 4”这种需要多工具的任务,看它怎么一步步完成。
4.4 参数校验层的加入
上面的代码还比较裸,加上校验层会更稳。我在工具执行前插入一个校验函数:
def validate_args(name: str, args: dict) -> tuple: if name == "get_weather": if "city" not in args or not isinstance(args["city"], str): return False, "参数 city 必须是非空字符串" if name == "calculate": if "expression" not in args or not isinstance(args["expression"], str): return False, "参数 expression 必须是字符串" return True, None执行工具前先调validate_args,不通过就把错误信息作为观察结果返回。这样模型下一轮会自动修正参数,而不是让程序崩溃。
4.5 记忆的持久化
短期记忆就是messages列表,任务结束就没了。如果要跨会话记住用户偏好,就得持久化。简单做法是把关键信息存进数据库或文件,下次任务开始时读出来拼进系统提示词。复杂做法是上向量库做语义检索。我一般先用简单方案,等确实有需求再升级,避免过度设计。
5. 常见问题与排查技巧实录
5.1 模型不调用工具,直接瞎编答案
这是最高频的问题。模型明明有工具可用,却直接凭记忆回答。原因通常是工具描述不够清晰,或者系统提示词没强调“必须用工具获取实时信息”。解决办法:在系统提示词里明确写“涉及实时数据时必须调用工具,不要凭记忆回答”,同时把工具描述写得更具体。我实测加这两条后,瞎编率大幅下降。
5.2 工具调用陷入死循环
模型反复调同一个工具,每次都得到相同结果,但就是不停。这通常是因为工具返回的结果模型“看不懂”或者认为“没解决问题”。排查思路:先看工具返回格式是否清晰,再看错误信息是否具体。如果工具返回{"success": false, "error": "查询失败"}这种模糊信息,模型会一直重试。改成{"success": false, "error": "数据库连接超时,请稍后重试"这种具体信息,模型就知道该放弃了。
另外,代码层的最大轮次限制是最后一道防线,一定要设。
5.3 参数格式错误导致工具执行失败
模型生成的 JSON 参数经常有各种问题:数字写成字符串、缺引号、多逗号。除了前面说的校验层,还可以在工具 schema 里把参数类型和格式描述得极其明确。如果某个工具参数特别容易出错,考虑简化参数结构,比如把多个参数合并成一个字符串让模型填,程序里再解析。
5.4 上下文超长导致响应变慢或报错
任务轮次多了,messages会越来越长。解决办法是滑动窗口:只保留系统提示词、最近 N 轮对话和当前任务相关的工具结果。更早的内容要么丢弃,要么摘要。我一般保留最近 6 轮,实测够用。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 模型不调工具直接回答 | 工具描述不清、提示词未强调 | 检查 description 和 system prompt |
| 反复调同一工具 | 工具返回信息模糊 | 让错误信息更具体 |
| 参数格式错误 | schema 描述不明确 | 简化参数、加校验层 |
| 响应变慢或报错 | 上下文过长 | 滑动窗口、摘要压缩 |
| 任务中途停止 | 达到最大轮次 | 调大轮次或优化任务拆解 |
| 工具执行超时 | 外部服务慢 | 加超时、返回明确错误 |
5.6 几个我踩过的坑
第一个坑是工具描述写得太技术化。我一开始按 API 文档的风格写 description,结果模型理解不了。后来改成大白话,明确说“这个工具用来干什么、什么时候用”,效果立竿见影。
第二个坑是忽略工具返回值的稳定性。早期我的工具有的返回字符串,有的返回字典,模型经常误判。统一成固定结构后,稳定性提升明显。
第三个坑是过早引入多 Agent。我一度觉得多 Agent 协作很酷,结果调试复杂度爆炸,两个 Agent 互相等待、消息传递丢失。后来退回单 Agent 加多工具,问题迎刃而解。多 Agent 不是不能用,但要在单 Agent 确实扛不住复杂任务时才考虑。
第四个坑是没有做成本监控。上线后才发现某些任务 token 消耗远超预期。后来加了每轮 token 统计和任务总成本记录,才把成本控制住。
6. 进阶方向与扩展思路
单 Agent 跑通之后,往哪走取决于你的业务需求。如果任务确实复杂到需要多个角色协作,可以引入多 Agent 架构,但要清楚它的代价:通信开销、状态同步、调试难度都会上升。我的建议是先用单 Agent 加工具分组的方式模拟多角色,实在不够再拆。
工具生态的扩展是更实际的方向。你可以把业务系统里的各种能力都封装成工具,让 Agent 成为统一的操作入口。这时候 MCP 这类标准化协议就有价值了,它能让工具定义在不同 Agent 之间复用,减少重复开发。
评估(evals)也是必须补的一环。Agent 不像传统程序,输出有随机性,你得有一套评估机制来判断它到底靠不靠谱。简单做法是准备一批测试任务,定期跑一遍看成功率。复杂做法是引入自动评估模型,对每次输出打分。我现在的项目每周跑一次回归测试,任务成功率低于阈值就告警。
至于多模态,如果你的场景涉及图片、语音,Agent 也可以接入多模态模型处理。但要注意,多模态会显著增加复杂度和成本,非必要不上。
最后说个学习路径的建议:不要一上来就啃框架源码,先把这篇里的最小循环手写一遍跑通,理解每一行在干什么。然后找一个自己真实的小需求去实现,比如自动整理文件、自动查数据生成报告。在真实需求里踩坑,比看十篇教程都管用。框架和高级特性,等你把基础打牢了再学,会快得多。