想弄清楚 AI Agent 开发的人,多半已经经历过这样一个阶段:Prompt 写了厚厚一叠,模型也换了好几个,但在真实业务场景里,Agent 仍然会“一本正经地胡说八道”,或者在执行到第三步时直接把上下文丢掉,甚至把不该调用的工具给调了。
如果你正卡在这个位置,这篇教程就是为你准备的。
很多人对 AI Agent 有一个误解:以为它只是“多轮 Prompt + 大模型调用”。但从实际工程来看,Agent 开发真正考验的是另一套能力——任务拆解、工具编排、状态管理、权限隔离和可观测性。这套能力决定了你写的 Agent 是只能在演示里跑通的 Demo,还是能放进企业业务里长期运行的“智能体”。
这篇文章我会从一个最小可运行的 Agent 出发,逐步拆解 Agent 开发的核心环节,最后落到企业级部署必须考虑的架构问题。读完你会有四条收获:
- 理解 AI Agent 的运行逻辑,而不是停留在“调 API”层面。
- 能手写一个带工具调用能力的最小 Agent,不依赖任何重框架。
- 知道企业级 Agent 与教学 Demo 的差异在哪里。
- 拿到一套可以直接复用的工程实践和排错思路。
1. AI Agent 开发到底难在哪里
先给一个明确的判断:Agent 开发的核心瓶颈不是模型,而是工程化。
单纯让大模型“聊天”并不难,难的是让大模型“负责完成一件事”。聊天只要生成文字,而 Agent 需要生成行动。什么算行动?调用 API 查订单、写入数据库、给用户推送消息、拉起另一个系统的工作流。一旦涉及行动,就会出现几个传统软件开发里并不常见的问题:
- 模型下一步做什么是概率性的,而不是确定性的。
- 模型会编造工具参数,比如把用户 ID 写成一个不存在的字符串。
- 多轮工具调用之后,上下文可能丢失或偏离原始目标。
- 工具数量一旦超过 10 个,模型选错工具的概率明显上升。
- 生产环境里你无法完全控制模型的所有行为,只能通过架构去约束。
这些问题不是换一个大模型就能解决的,它们需要在 Agent 的框架设计层面处理。这也是为什么“会写 Prompt”和“会做 Agent 开发”是两种完全不同的能力。前者是在跟模型对话,后者是在围绕模型构建一套带约束的自动执行系统。
2. AI Agent 核心概念与运行逻辑
2.1 Agent 的本质是一个循环
先放下那些复杂的术语。AI Agent 最核心的运行模式,是一个“思考 → 调用工具 → 观察结果 → 再思考”的循环。
完整过程可以描述为:
- 接收用户目标。
- 大模型根据目标和已有信息,决定下一步动作:直接给出回答,或者调用某个工具。
- 如果调用工具,则执行对应函数,把返回值交给模型。
- 模型把工具结果纳入上下文,继续下一步决策。
- 直到模型认为目标已经完成,输出最终答案。
理解这个循环,是 Agent 开发的第一个分水岭。很多新手写的“Agent”其实只有第一步和最后一步,中间没有工具执行和结果反馈,自然也就没有“智能”可言。
2.2 四大核心组件
从工程抽象的角度看,一个 Agent 系统由四类组件组成。
| 组件 | 作用 | 类比 |
|---|---|---|
| LLM | 负责理解和决策 | 员工的大脑 |
| Tools | 让 Agent 能影响外部系统 | 员工的双手 |
| Memory | 保存对话历史和关键信息 | 员工的笔记本 |
| Planner/Runner | 控制任务分解和循环执行 | 项目经理的排期表 |
这里的 Tools 是 Agent 开发里最值得琢磨的部分。所谓 Tool,不只是普通的 API 封装函数,而是要附带一段“使用说明”给模型看。大模型就是靠这个说明判断“什么场景该用哪个工具”。工具说明写得越准确,模型的调用正确率越高。这也是 Agent 开发里“提示词工程”真正发挥作用的地方——它不是用在系统 Prompt 上,而是用在工具描述上。
2.3 Function Calling 是关键机制
Function Calling(函数调用)是当前大多数 Agent 技术方案采用的标准机制。模型在生成普通文本的同时,可以输出一个结构化 JSON,里面包含工具名称和参数。之后由你的业务代码去真正执行这个函数,再把结果拼回对话上下文。
如果模型没有选择调用任何函数,它输出的就是最终回复。所以,判断一次 Agent 执行是否结束,最直接的标志就是:这一轮返回结果里是否包含 tool_calls 字段。
2.4 Agent 与普通 API 调用的区别
普通 API 调用是“输入 → 输出”的短链路,一次请求得到一个结果。Agent 则是“输入 → 多轮内部调用 → 输出”的长链路,中间可能穿插多次工具执行,而且每轮决策都依赖上一轮的结果。这也是为什么 Agent 的调试比普通接口开发困难很多:你不仅要看大模型的最终输出,还要看它每一轮为什么选择这个工具、参数是否正确、返回结果是否被正确理解。
3. 企业级 Agent 与 Demo Agent 的分水岭
如果一个 Agent 只用于教学演示,你可以容忍它偶尔出错,甚至可以手动在对话里纠正它。但企业级 Agent 的要求完全不同,它直接面对真实用户和真实系统,错误是有成本的。
企业级 Agent 与 Demo Agent 的核心差异,可以用一张表看清楚:
| 维度 | Demo Agent | 企业级 Agent |
|---|---|---|
| 工具权限 | 全量放开 | 按角色最小授权 |
| 错误容忍度 | 允许重试 | 需要兜底和降级 |
| 可观测性 | 打印日志 | 全链路追踪 + 审计 |
| 上下文管理 | 全量塞给模型 | 截断、摘要、长期记忆 |
| 评估方式 | 人工看结果 | 测试集回归 + 指标对比 |
| 安全边界 | 几乎不考虑 | 输入过滤、工具白名单、参数校验 |
从实际项目经验看,企业级 Agent 最容易被忽略的环节是“工具调用的参数校验”。大模型生成的参数看起来格式正确,但值可能是乱来的。比如查订单接口,模型传入了一个“2024-13-45”这样的日期。所以在 Agent 的工程实现里,每个工具的入参都必须有独立的业务层校验,不能假设模型返回的参数永远正确。
4. 环境准备与前置条件
工欲善其事,必先利其器。本文的示例采用 Python 编写,因为 Agent 开发相关的生态最成熟的语言仍然是 Python。
4.1 运行环境
- 操作系统:Windows 10/11、macOS 或 Linux 均可。
- Python 版本:建议 3.10 及以上,具体以实际环境为准,本文代码基于通用 Python 语法编写。
- 包管理:使用 venv 或 conda 创建独立虚拟环境。
- 模型接口:本文使用 OpenAI SDK 的通用调用方式,可对接兼容该接口的模型服务。请根据你所在团队的技术选型,配置合法的模型服务访问方式。
环境准备的最小命令:
mkdir agent-demo cd agent-demo python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate pip install openai如果你希望完全本地化运行,也可以选择 Ollama 等本地推理方案,并暴露 OpenAI 兼容接口。需要注意的是,本地小参数模型的工具调用成功率通常低于云端大模型,做教学演示可以,做生产级应用时建议根据需要评估模型能力。
4.2 依赖说明
本文两个核心依赖:
- openai:用于调用大模型接口,并解析 tool_calls。
- 标准库 json、datetime、logging:用于工具参数解析、日期处理和日志记录。
这里刻意不引入 LangChain、LlamaIndex 等重量级框架,目的是让你先看清 Agent 运行的最底层原理。框架确实能提升开发效率,但如果连最基础的循环都没理解就一头扎进框架,出了问题会非常被动。
5. 从零手写一个最小 Agent:先跑通再谈架构
这一章,我们实现一个真正能运行的最小 Agent。目标只有一个:让 Agent 具备调用工具的能力,并且跑通“思考 → 调用 → 观察 → 再思考”的完整循环。
5.1 项目结构
agent-demo/ ├── venv/ ├── main.py └── requirements.txtrequirements.txt 内容:
openai>=1.0.05.2 最小 Agent 核心代码
在 main.py 中写入以下代码:
import json from openai import OpenAI # 初始化客户端 # 如果你使用 OpenAI 官方服务,按合规要求配置 API Key # 如果你使用兼容 OpenAI 接口的本地或云端网关,填对应的 base_url client = OpenAI( api_key="your-api-key", base_url="your-openai-compatible-endpoint", # 例如本机推理服务的 /v1 地址 ) # 1. 定义工具 tools = [ { "type": "function", "function": { "name": "calculate", "description": "执行四则运算,输入一个算式字符串,例如 12*7+5", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "要计算的数学表达式" } }, "required": ["expression"] } } } ] # 2. 定义工具的实际执行函数 def calculate(expression: str) -> dict: # 生产环境不要用 eval,这里仅做最小示例 # 生产环境建议用白名单解析器或 ast 模块做安全校验 try: result = eval(expression) return {"success": True, "result": result} except Exception as e: return {"success": False, "error": str(e)} # 工具分发 def dispatch_tool(name: str, arguments: str): args = json.loads(arguments) if name == "calculate": return calculate(args["expression"]) return {"success": False, "error": f"未知工具: {name}"} # 3. 核心循环 def run_agent(user_input: str, max_steps: int = 5): messages = [{"role": "user", "content": user_input}] for step in range(max_steps): response = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools, ) message = response.choices[0].message messages.append(message) # 没有工具调用,表示 Agent 已输出最终答案 if not message.tool_calls: return message.content # 执行每个工具调用 for tool_call in message.tool_calls: print(f"[step {step + 1}] 调用工具: {tool_call.function.name}, 参数: {tool_call.function.arguments}") result = dispatch_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) return "已达到最大执行步数,任务可能未完成。" # 4. 运行测试 if __name__ == "__main__": answer = run_agent("请计算 12345 * 6789 等于多少") print("最终答案:", answer)5.3 关键逻辑解释
这段代码里最核心的设计有四个:
第一,tools 列表是给模型看的“使用说明书”。模型不会直接执行你的函数,它只负责输出“我想调用 calculate,参数是 xxx”。真正执行的人是你的 dispatch_tool。
第二,messages 列表一直在累积。每次工具调用的结果都以 role=tool 的身份追加进去,模型下一轮才能“看到”工具返回了什么。
第三,循环终止条件有两个:模型不再生成 tool_calls,说明它认为已经有足够信息回答;另一是达到 max_steps,防止 Agent 陷入无限循环。
第四,temperature、max_tokens 这类参数这里没有设置,因为在 Agent 场景下,工具调用往往比创造性回答更重要,一般建议把 temperature 调低(如 0.2 或 0),以减少随机性。但这里保持最小示例的简洁,暂不展开。
5.4 运行与验证
python main.py预期输出类似:
[step 1] 调用工具: calculate, 参数: {"expression": "12345*6789"} 最终答案: 83810205注意:真正的输出取决于模型的行为,模型可能在第一轮就调用工具,也可能先输出一段说明再调用工具。只要最终能看到“调用工具”的日志以及“最终答案”,就说明这个最小 Agent 已经跑通了。
如果运行失败,优先检查三处:
- base_url 和 api_key 是否配置正确。
- 模型名称是否与你的模型服务匹配。
- 模型是否支持 tool_calls / function calling 能力。
6. 给 Agent 接入真实业务工具:订单查询示例
上一章的 calculate 工具只是验证流程,接下来模拟一个更接近企业业务的场景:用户询问订单状态,Agent 需要调用订单查询 API。
这个示例会体现三个工程要点:工具描述要写清楚参数约束、工具内部要做参数校验、返回值要结构化。
6.1 模拟订单工具
import json import datetime # 模拟订单数据源 FAKE_ORDERS = { "A1001": {"status": "已发货", "eta": "2025-03-18", "goods": "机械键盘"}, "A1002": {"status": "待支付", "eta": None, "goods": "显示器支架"}, } def query_order(order_id: str) -> dict: # 参数校验:模型可能传空字符串、None 或非法格式 if not order_id or not isinstance(order_id, str): return {"success": False, "error": "订单号不能为空"} # 校验订单号格式:A + 4 位数字 if len(order_id) != 5 or not order_id.startswith("A") or not order_id[1:].isdigit(): return {"success": False, "error": "订单号格式不正确,应为 A 加 4 位数字"} order = FAKE_ORDERS.get(order_id) if not order: return {"success": False, "error": f"未找到订单 {order_id}"} return {"success": True, "data": order}对应的工具描述:
order_tool = { "type": "function", "function": { "name": "query_order", "description": "根据订单号查询订单状态。订单号格式为 A 开头加 4 位数字,例如 A1001。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "用户提供的订单号,形如 A1001" } }, "required": ["order_id"] } } }注意工具描述里特意写清了订单号格式。这不是多余的说明,而是降低模型乱传参数概率的关键手段。模型不是数据库,它对“订单号应该长什么样”没有概念,只有你在描述里显式写清楚,它才能生成符合格式的参数。
6.2 组装新的工具列表
tools = [order_tool] def dispatch_tool(name: str, arguments: str): args = json.loads(arguments) if name == "query_order": return query_order(order_id=args.get("order_id")) return {"success": False, "error": f"未知工具: {name}"}6.3 验证效果
执行:
if __name__ == "__main__": answer = run_agent("我想查一下订单 A1001 到哪了") print("最终答案:", answer)如果 Agent 正常执行,它应该调用 query_order 工具,然后根据返回结果告诉用户“订单 A1001 已发货,预计 2025-03-18 送达,商品是机械键盘”。
这里真正容易踩坑的是:用户消息里可能携带“订单号是A1001、帮我查一下”这种非标准表达。模型需要从自然语言里抽取订单号并填进工具参数。如果抽错了,需要靠工具内部的校验逻辑兜底。所以 Agent 开发并不是“写一个函数让模型调用”那么简单,工具自身的健壮性会直接决定 Agent 的可用性。
7. 企业级 Agent 落地架构建议
跑通最小示例之后,距离企业级应用还有一段很长的路。这一章我给出一个相对完整的 Agent 架构分层方案,每一项都是生产环境迟早要面对的问题。
7.1 路由与编排层
企业内通常不止一个 Agent,可能有客服 Agent、数据分析 Agent、运维 Agent。用户请求进来之后,第一层需要一个 Router 来决定把请求交给哪个 Agent,或者哪个模型。这不是一个简单的 if-else,实践中常用两种方式:
- 基于分类模型的路由:用一个小模型判断用户意图。
- 基于关键词与规则的路由:适合意图边界清晰的场景。
路由层的好处是隔离故障。假设数据分析 Agent 依赖的数据库抖动,至少客服 Agent 还能正常工作。
7.2 会话状态与记忆层
生产环境的 Agent 不能每次请求都从零开始。你需要维护:
- 短期记忆:当前会话最近 N 轮消息。
- 长期记忆:用户偏好、历史订单、历史决策,通常存在向量数据库或 Redis 中。
- 工作记忆:当前任务上下文,比如已经查到的订单信息。
记忆管理的难点是上下文窗口有限。当对话超过模型窗口长度时,不能简单截断,而要考虑对历史消息做摘要,或者把关键信息抽取成结构化字段。从实践看,用摘要压缩历史消息的效果通常优于逐字截断,因为它保留了语义重点。
7.3 权限与安全层
这是企业级 Agent 和 Demo 最本质的区别。Agent 一旦接入企业内部系统,工具的调用权限必须分级。
我建议的最小权限体系如下:
| 用户角色 | 可调用工具 | 是否需要审批 |
|---|---|---|
| 普通用户 | 查询类工具 | 否 |
| 运营人员 | 查询 + 部分写入类工具 | 否 |
| 管理员 | 全部工具 | 高风险操作需要审批流 |
高风险工具(如退款、删除数据、修改配置)在执行前必须经过二次确认。具体实现上,可以在工具分发层加一个 permission 装饰器,没有权限直接返回错误,而不是把请求发给模型处理。
7.4 可观测性与审计
Demo 阶段,一句 print 日志足够了。生产环境不行。
你需要记录每一次 Agent 执行的:
- 用户输入。
- 模型每一轮输出。
- 调用了哪些工具,参数是什么。
- 工具返回结果是什么。
- 最终响应是什么。
- 每步耗时、Token 消耗。
这些数据至少有三个用途:排查线上问题、评估模型表现、满足企业内部审计要求。实际项目中,团队会把 Agent 执行过程输出为结构化日志,写入集中式日志平台,再配一套 Dashboard 查看工具调用成功率、模型回答耗时等指标。
7.5 评估与回归
企业级 Agent 上线前一定要有一组测试用例。比如 50 个典型问题,以及每个问题期望的工具调用序列和最终答案。模型升级或 Prompt 修改后,先跑回归测试。AI Agent 是概率系统,没有回归测试兜底,一次 Prompt 调整可能让线上表现全面退化,而你未必能及时察觉。
7.6 配置示例
下面给出一份参考配置,用 YAML 描述一个企业级 Agent 的核心参数:
agent: name: order-service-agent version: 1.0.0 model: provider: openai-compatible base_url: your-gateway-address model_name: your-model-name temperature: 0.1 max_tokens: 2048 timeout_seconds: 30 loop: max_steps: 8 enable_early_stop: true memory: type: redis ttl_seconds: 3600 max_history_rounds: 10 tools: query_order: permission: user timeout_seconds: 3 cancel_order: permission: admin need_approval: true logging: level: INFO include_tool_args: true sink: elasticsearch这份配置相对抽象地体现了:模型参数、执行步数、记忆策略、工具权限和日志输出。实际接入时还需要根据企业技术栈做细化,比如 Redis 连接信息、日志平台地址、审批流回调地址等。
8. Agent 开发常见问题与排查方法
Agent 开发调试成本高,一个重要原因是“错误链”很长。问题可能出在 Prompt、工具描述、参数解析、模型能力或外部系统,任何一个环节出错都会导致最终结果异常。以下是实践中频率最高的问题:
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 模型不调用工具 | 工具描述不清晰,或者模型版本不支持 Function Calling | 查看模型原始返回,确认是否支持 tool_calls | 重写工具描述,换成支持函数调用的模型 |
| 工具参数总是传错 | 工具 description 里没有写清字段格式约束 | 打印模型生成的 arguments 原文 | 在描述中补充格式示例,如“订单号为 A 加 4 位数字” |
| Agent 反复调用同一个工具 | 工具返回结果模型没理解,或返回结构太复杂 | 查看工具返回值是否被正确拼进上下文 | 简化返回值格式,增加 success/error 字段 |
| 上下文超出窗口报错 | 长期对话或工具返回内容过长 | 查看 token 消耗日志 | 加入历史截断、关键信息压缩、减少每轮工具返回内容 |
| 最终答案明显偏离目标 | 多轮循环后任务漂移 | 检查每一轮 messages 内容,观察系统 Prompt 是否被覆盖 | 引入任务阶段追踪,必要时重新约束目标 |
| 工具执行报错但 Agent 不自知 | 工具异常返回格式不规范 | 确认工具异常分支是否返回结构化字典 | 所有工具必须返回统一结构:success / data / error |
| 模型速度太慢 | 单轮生成 token 太多,或循环次数过多 | 查看每轮生成耗时 | 降低 max_tokens、限制工具返回长度、控制 max_steps |
排查 Agent 问题有一条基本思路:从头回放每一轮模型输出。不要只看最终答案,而是把 messages 列表完整打印出来。每一步模型用了什么工具、带了什么参数、工具返回了什么,看完基本能定位问题。
9. Agent 开发最佳实践与学习路线
9.1 五条工程建议
第一,工具描述要像写接口文档一样认真。对 Agent 来说,工具描述就是它操作世界的说明书。描述里要有参数格式、边界条件、错误示例。描述质量直接决定工具调用成功率。
第二,所有工具返回值统一结构。推荐使用 {"success": boolean, "data": ..., "error": ...} 三段式结构。这样模型解析起来简单,日志和监控也好做。
第三,默认调低 temperature。Agent 执行内部工具调用时,我们希望它稳定、可预测,而不是充满创造力。temperature 建议 0 到 0.3 之间。如果模型总是犯错,先降低 temperature,再优化 Prompt。
第四,一定要有最大步数保护。一个 Agent 循环如果没有步数上限,遇到模型反复思考的极端情况会浪费大量 Token 和时间。建议默认 5 到 10 步,根据业务复杂度调整。
第五,生产环境禁止放开所有工具。逐个上线、逐个验证。可以把工具按“查询类”和“写入类”分开,查询类先上线,写入类经过审批流后再放开。
9.2 学习路线建议
如果你是想系统学习 Agent 开发的新手,我建议按这套顺序:
- 先不依赖框架,用原生模型接口手写一个带工具调用的最小 Agent。
- 理解 ReAct 模式、Function Calling、记忆压缩等核心概念。
- 再用 LangChain 或 LlamaIndex 提升开发效率,但始终保持能看懂底层实现。
- 动手做一个真实业务场景的 Agent,比如客服问答、订单查询、数据分析助手。
- 最后补上可观测性、评估、权限这三个企业级模块。
很多人的误区是一开始就上 LangChain,结果被各种抽象概念绕晕,出了问题不知道是框架问题还是模型问题。先手写循环,再上框架,你在排查问题时就会清晰得多。
9.3 关于“一周吃透”的客观提醒
“一周吃透 AI Agent”这个说法,我建议用更务实的眼光来看。一周时间足够你理解核心概念、跑通最小示例、写出第一个带工具调用的 Agent。但要达到企业级水平,还需要在实际业务里积累经验。真正拉开差距的不是你想了多少概念,而是你在真实项目中处理过多少异常、调过多少工具、踩过多少坑。
如果你能把本文的最小 Agent 扩展成一个能处理用户真实消息、调用多个企业工具、并且导出执行日志的系统,你已经具备了 Agent 开发的核心能力。剩下的,就是在项目里持续打磨。
建议先按顺序跑通第五章和第六章的代码,再尝试把系统 Prompt、工具描述、日志输出调整成适合你自己业务的形式。遇到问题,回到第八章的排查表逐个对照。这一套走完,你对 AI Agent 开发的理解会超过大多数停留在概念阶段的人。