
如果你正在把一个“调用什么工具、按什么顺序调用”的决策交给大模型来做,而不是在代码里写死 if-else,那你其实已经在接触 AI Agent 了。过去一年,Agent 这个概念从 demo 走进了越来越多的生产系统。很多人以为 Agent 开发就是“调大模型 API 加一个循环”,但真到了接业务工具、控制权限、处理失败重试的时候,才发现问题并不是“模型不够聪明”,而是工程边界没有设计清楚。这篇文章想解决的就是这个问题:不依赖某个特定框架,也不绑定某家厂商,而是把 Agent 的底层运行机制拆开讲清楚,然后带你从零搭一个最小可用的“模型 工具”编排系统。读完你至少能收获三件事:理解 Agent 的核心循环到底在循环什么;学会用代码实现工具注册、模型调用和结果解析;知道生产环境里哪些地方最容易出问题,以及怎么提前规避。1. 这篇文章真正要解决的问题现在市面上的 Agent 框架很多,有面向对话的,有面向编程的,有面向浏览器操作的。但如果你仔细观察,会发现它们的底座几乎都一样:一个能推理的大模型,一组外部工具,以及一个控制循环。框架只是把这三样东西的交互方式封装成了更友好的接口。既然如此,为什么还要自己动手写一个最小实现?因为直接用框架,很多关键细节会被隐藏。比如工具调用的参数是从哪来的、模型返回的 JSON 被截断怎么办、工具执行超时到底是继续等还是重试、上下文越来越长之后怎么压缩。这些问题不是框架不处理,而是框架的默认行为不一定适合你的业务场景。你自己写一遍循环逻辑,才能真正理解这些抽象背后的取舍。这篇文章更适合以下几类读者:后端开发者,想把大模型接入现有业务系统,而不是只做一个聊天机器人。已经在用 LangChain、Coze、Dify 等平台,但遇到“配置能跑、生产就崩”问题的人。想深入理解 Agent 原理,而不是停留在“Agent 就是让 AI 自己干活”这个层面的人。判断很明确:Agent 开发的门槛,不在于调用大模型 API,而在于设计好“模型、工具、记忆”三者之间的边界。边界清晰,Agent 才有机会稳定运行;边界模糊,再强的模型也会变成随机数生成器。2. Agent 的核心概念与底层原理2.1 Agent 与传统 API 调用的区别传统开发里,你写一个函数,输入明确,输出明确,流程固定。比如“根据用户手机号查订单状态”,代码里就是一步数据库查询,最多加几个异常分支。Agent 的场景则不同。用户说“帮我看看这个订单有没有异常,如果有就发一封提醒邮件”。这句话本身没有明确告诉你“先查订单,再判断异常,再发邮件”,也没有告诉你“异常的标准是什么”。放在传统代码里,你需要把这句话拆成穷举的规则;放在 Agent 里,你只需要提供工具“查询订单详情”和“发送邮件”,然后让模型自己决定调用顺序。可以把传统方式理解为“精确指令”,把 Agent 理解为“目标驱动 工具调用”。2.2 Agent 循环的五要素一个最小可用的 Agent,通常包含下面五个部分:大模型:负责理解任务、拆解步骤、决定调用哪个工具。工具注册表:把“可被模型调用的函数”集中管理,每个工具都有名字、描述、参数格式。循环控制:模型输出行动意图 - 执行工具 - 把结果给回模型 - 模型判断是否继续。记忆:短期记忆是当前对话上下文;长期记忆是向量数据库或外部存储。停止条件:模型输出最终答案,或者达到最大迭代次数。2.3 最容易误解的概念:工具调用不是“模型直接执行代码”很多新手会把“工具调用”理解为“大模型直接执行 Python 代码”。实际不是。大模型做的事情,是输出一段结构化文本,比如“我想调用 get_order 工具,参数是 order_id12345”。你的代码解析这段文本,然后调起真正的 get_order 函数。也就是说,模型是“决策者”,你的程序才是“执行者”。这个区别非常关键:工具的执行权限、参数校验、异常处理,都必须由你的代码负责,不能依赖模型自觉。对比维度传统规则流程Agent 决策流程流程是否固定固定,写死分支动态,由模型决定工具接入方式代码直接调用通过工具注册表暴露给模型异常处理开发者手动穷举模型推理 代码兜底适合场景规则明确、稳定目标开放、多变主要风险覆盖不全不可控、上下文溢出、权限过大3. 环境准备与前置条件在开始写代码之前,先把环境梳理清楚。这篇文章的示例偏通用实现,不锁定某个具体框架,所以你不需要安装重型依赖。3.1 运行环境Python 3.10 或更高版本。Agent 循环涉及大量类型注解和异步逻辑,新版本 Python 会更顺手。一个可用的 OpenAI 兼容 API 服务。无论你用的是云端大模型还是本地部署模型,只要它支持/v1/chat/completions接口和 function calling,就可以接入本文示例。pip 管理的虚拟环境。建议使用venv或uv创建独立环境,避免依赖冲突。3.2 依赖库这里不引入 LangChain 之类的重框架,只用两个基础库:pip install openai python-dotenvopenai是官方的 SDK,用来调用兼容接口;python-dotenv用来加载环境变量,把 API 地址和密钥放到.env文件中,避免写死在代码里。3.3 版本说明本文不会把某个库的版本号写死,因为大模型工具链迭代非常快。你在安装时以当前最新稳定版为准即可。如果遇到接口兼容性问题,优先查看该库的官方迁移文档。3.4 准备一个最小工具为了让示例聚焦在 Agent 机制上,我们不需要真实的订单系统或邮件服务。准备两个模拟工具就好:get_user_info(user_id):返回用户信息。send_notification(user_id, message):模拟发送通知。在实际项目中,这两个函数会被真实业务接口替代,但它们的工具描述方式和注册逻辑完全一致。4. Agent 核心流程拆解现在把 Agent 的运行流程拆成六个步骤。理解这六步,你就掌握了绝大多数 Agent 框架的底层逻辑。4.1 第一步:组装系统提示词系统提示词告诉模型“你是谁、你能用哪些工具、你该以什么格式输出”。工具描述通常从工具注册表动态生成,而不是手写在提示词里,这样每次调整工具列表时不用修改大段文案。系统提示词常见的坑是写得过于宽松。比如只写“你可以调用工具”,却没说清楚“什么情况下必须调用工具”“参数缺失时怎么办”。建议在提示词里明确两条规则:第一,如果需要的信息不足,先通过工具获取,不要编造;第二,如果工具返回错误,直接告诉用户失败原因,不要强行包装成成功。4.2 第二步:模型输出结构化行动把用户问题和工具定义一起发给模型后,模型可能输出两类内容:一是最终答案,二是“我要调用某个工具”的结构化请求。在 OpenAI 兼容接口里,后者通常对应tool_calls字段,里面包含工具名称和参数。这里需要做好防御:有些模型在长上下文场景下会输出残缺 JSON,参数名也可能和工具定义不完全一致。你的解析层要能容忍这些小偏差,而不是直接崩溃。4.3 第三步:执行真实工具解析出工具名和参数后,你的代码从注册表找到对应函数,完成参数校验,然后执行。工具函数内部可能有网络请求、数据库查询、文件读写,这些副作用要能幂等重试。工具执行阶段是最容易出安全事故的地方。一个常见的错误是直接把模型传入的参数原样透传给数据库查询或 shell 命令。正确做法是:白名单校验、类型校验、权限校验,再加一层审计日志。4.4 第四步:把工具结果返回给模型工具执行完,把结果作为一条tool消息追加到对话里,再次发给模型。模型看到工具结果后,可能继续调用下一个工具,也可能直接整理答案。这个“行动-观察-再行动”的循环,就是 Agent 的核心。循环本身不复杂,复杂的是如何控制循环次数、如何检测死循环、如何合并过长结果。4.5 第五步:判断停止条件每轮循环都要检查停止条件,通常是以下三种情况之一:模型输出最终答案且不再请求工具;达到最大迭代次数;工具连续多次返回同一类错误。最大迭代次数的设置很关键。设太小,复杂任务会半途而废;设太大,可能出现“模型反复调同一个失败工具”的浪费。从实践看,普通任务 5 到 10 轮足够,复杂任务可以放宽,但要配合超时控制。4.6 第六步:结构化输出与结果返回最终答案不一定适合直接返回给用户。如果下游系统是程序,建议让模型按 JSON 格式输出关键字段;如果下游是人,可以保留自然语言,同时附加工具执行摘要。结构化输出在生产环境很常用,但要注意:模型输出的 JSON 也可能不合法。不要假设模型一定遵守格式,要做一次容错解析和补全。5. 完整示例代码实现下面我们动手实现一个轻量级 Agent。这个实现不依赖 LangChain,核心代码只有一百行左右,方便你理解机制,也方便迁移到自己的项目里。5.1 工具注册表设计工具注册表的核心是“统一格式”。每个工具都要有名字、描述、参数 JSON Schema,以及对应的 Python 函数。模型看到的是注册表生成的描述,代码执行的却是真实函数。# 文件路径:agent_tools.py from typing import Callable, Dict, Any, Optional class Tool: def __init__(self, name: str, description: str, parameters: dict, func: Callable): self.name name self.description description self.parameters parameters self.func func def to_openai_tool(self) - dict: 转换为 OpenAI function calling 需要的格式 return { type: function, function: { name: self.name, description: self.description, parameters: self.parameters, }, } def run(self, **kwargs) - str: 执行工具函数并把结果转为字符串 result self.func(**kwargs) if isinstance(result, (dict, list)): import json return json.dumps(result, ensure_asciiFalse) return str(result)关键逻辑有两个:一是to_openai_tool,把内部工具描述转换成模型接口需要的结构;二是run,把工具返回值统一序列化成字符串。统一字符串格式非常重要,因为模型接收工具结果时,只把它当作文本,不会关心你是 dict 还是对象。5.2 实现两个模拟业务工具这里的两个工具是占位实现。真实项目中,get_user_info可能换成查数据库,send_notification可能换成调消息网关。# 文件路径:mock_business_tools.py from agent_tools import Tool def get_user_info(user_id: str) - dict: 模拟从用户中心获取用户信息 if user_id 001: return {user_id: 001, name: 张三, level: vip, order_count: 12} return {user_id: user_id, name: 未知用户, level: normal, order_count: 0} def send_notification(user_id: str, message: str) - dict: 模拟发送站内信 print(f[通知] 向用户 {user_id} 发送: {message}) return {success: True, user_id: user_id, message_length: len(message)} user_tool Tool( nameget_user_info, description根据用户ID查询用户基本信息和订单数量, parameters{ type: object, properties: { user_id: {type: string, description: 用户ID} }, required: [user_id], }, funcget_user_info, ) notify_tool Tool( namesend_notification, description向指定用户发送一条站内信通知, parameters{ type: object, properties: { user_id: {type: string, description: 接收通知的用户ID}, message: {type: string, description: 通知内容}, }, required: [user_id, message], }, funcsend_notification, )这里要特别注意参数描述。模型决定调用哪个工具、生成什么参数,靠的就是工具描述和参数描述。描述越模糊,模型越容易传错参数。比如user_id要写清楚是“用户ID”而不是“订单号”,message要写清楚是“站内信内容”而不是“系统日志”。5.3 核心 Agent 循环现在实现 Agent 主体。它做的事情很简单:把用户问题和工具列表发给模型;如果模型返回工具调用请求,就执行工具并把结果追加回对话;直到模型输出最终答案。# 文件路径:simple_agent.py import json from openai import OpenAI from agent_tools import Tool class SimpleAgent: def __init__(self, client: OpenAI, tools: list[Tool], model: str gpt-4o): # model 名称请替换为你实际可用的模型 self.client client self.tools tools self.model model self.tool_map {tool.name: tool for tool in tools} self.max_iterations 6 def _build_tools(self) - list[dict]: return [tool.to_openai_tool() for tool in self.tools] def _call_model(self, messages: list[dict]) - dict: response self.client.chat.completions.create( modelself.model, messagesmessages, toolsself._build_tools(), tool_choiceauto, ) return response.choices[0].message def _execute_tool(self, tool_call) - str: tool_name tool_call.function.name arguments json.loads(tool_call.function.arguments or {}) tool self.tool_map.get(tool_name) if tool is None: return f错误: 未知工具 {tool_name} # 这里可以做参数白名单校验,生产环境建议增加 try: result tool.run(**arguments) return result except Exception as e: return f错误: 工具执行失败 {str(e)} def run(self, user_input: str) - str: messages [ { role: system, content: 你是业务助手。如果用户提到查用户或发通知,必须调用对应工具。 如果工具返回错误,直接告诉用户失败原因。, }, {role: user, content: user_input}, ] for _ in range(self.max_iterations): message self._call_model(messages) if not message.tool_calls: return message.content or # 把模型这次带工具调用的消息加入历史 messages.append(message) # 执行所有工具调用,并把结果追加为 tool 消息 for tool_call in message.tool_calls: tool_result self._execute_tool(tool_call) messages.append( { role: tool, tool_call_id: tool_call.id, content: tool_result, } ) return 已达到最大迭代次数,任务未完成。这段代码的核心是messages.append(message)和后面的tool消息拼接。如果你漏掉了模型带工具调用的消息,接口会报错,因为tool_call_id无法对应到上一条模型消息。这是新手最容易踩的坑。5.4 环境配置与启动入口把 API 地址、密钥和模型名放在.env文件里,然后写一个简单的入口脚本。# 文件路径:.env OPENAI_API_KEYsk-你的密钥 OPENAI_BASE_URLhttps://你的兼容服务地址/v1 AGENT_MODELgpt-4o# 文件路径:main.py import os from dotenv import load_dotenv from openai import OpenAI from simple_agent import SimpleAgent from mock_business_tools import user_tool, notify_tool load_dotenv() client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL), ) agent SimpleAgent( clientclient, tools[user_tool, notify_tool], modelos.getenv(AGENT_MODEL), ) if __name__ __main__: # 测试1: 查询用户信息 result1 agent.run(帮我查一下用户 001 的基础信息) print(结果1:, result1) # 测试2: 查完用户后发送通知 result2 agent.run(先查用户 001 的信息,然后给ta发送一条 VIP 续费提醒) print(结果2:, result2)注意,OPENAI_BASE_URL指向的是你的大模型服务地址,路径里要带/v1。如果用的是本地部署模型,可以填http://localhost:8000/v1。密钥、模型名都不要写死在 Python 代码里,这是最基本的配置管理习惯。6. 运行结果与效果验证6.1 运行方式在项目目录下执行:python main.py如果一切正常,第二次测试的日志应该类似:[通知] 向用户 001 发送: VIP 续费提醒 结果2: 已向用户 001 发送 VIP 续费提醒,发送成功。6.2 如何判断成功判断 Agent 是否正常工作,不能只看最终输出,还要跟踪中间过程。建议先跑一次刚才的测试,确认三件事:模型是否正确识别出“需要调用工具”。工具参数是否从用户语句中正确抽取。工具执行结果是否回流给模型,并影响了最终回答。你可以临时在_execute_tool里加一行打印,把模型传入的原始参数打出来。如果模型传错了参数名,你会立刻发现是参数描述写得不够清楚。6.3 失败时先看哪里如果结果不符合预期,按这个顺序排查:看 API 返回的原始响应,确认tool_calls是否存在。如果模型直接输出文字而没有调用工具,说明系统提示词或工具描述没有生效。看工具执行阶段有没有抛异常。多数问题出在参数类型或字段名不匹配。看第二轮请求是否携带了tool消息。如果消息结构不正确,接口会返回类似tool_call_id not found的错误。7. 常见问题与排查思路问题现象可能原因排查方式解决方案模型一直不调用工具系统提示词太含糊或工具描述不清晰打印模型原始返回,观察是否出现 tool_calls在提示词中明确“必须先调用工具才能回答”;补全工具描述模型生成 JSON 参数解析失败参数被截断或模型不支持复杂 JSON 格式打印 arguments 原文,检查是否为完整 JSON使用兼容性更好的模型;增加 JSON 容错解析;对参数做长度限制工具执行后模型“失忆”忘记把 tool 消息追加到对话历史检查日志中第二轮 messages 是否包含 tool 消息确保每个 tool_call 都对应一条 tool 消息,并带上 tool_call_id出现死循环,反复调同一工具工具返回错误,但模型仍在尝试检查工具返回内容是否包含错误原因工具结果中主动说明错误;设置最大迭代次数和连续失败中止逻辑上下文越来越长,请求超时历史消息未做截断或摘要查看 messages 的 token 数量对历史消息做截断;长任务改为子任务拆分工具权限过大,模型传入危险参数参数未做白名单校验检查工具函数是否直接透传参数增加参数校验、权限校验和审计日志多工具同时调用时部分失败部分工具结果未追加完整检查循环中 tool_calls 是否遍历完整确保每个 tool_call 都执行并追加结果模型输出最终答案但仍带工具调用停止条件判断不严谨检查是否同时处理了 content 和 tool_calls只有在tool_calls为空时才返回最终答案这些排错经验来自一个常见事实:Agent 大部分问题不是“模型笨”,而是“消息结构错”。你在自研 Agent 时,保留原始请求日志和响应日志,会节省大量排查时间。8. Agent 开发的最佳实践与工程建议8.1 工具设计:小而专,描述要准工具函数最好只做一件事。一个“万能工具”会让模型难以判断什么时候调用、传什么参数。建议每个工具的参数不超过 5 个,参数描述中写清楚含义、取值范围和示例。工具返回内容也不要过于冗长。模型从工具结果中提取信息时,结果越长越容易出错。能返回摘要就返回摘要,能截断就截断。8.2 提示词工程:版本管理不能省系统提示词是 Agent 行为的重要控制器,但它和代码一样会演化。建议把提示词模板作为独立文件存放,纳入 Git 管理,并记录每次修改对线上效果的影响。不要只在代码里改字符串,否则两周后你就分不清哪一版提示词对应哪一版行为。8.3 可观测性:每个循环都要留痕Agent 的每个循环都涉及一次模型调用和一次工具执行。生产环境必须记录以下信息:模型请求的消息内容。模型返回的原始响应。工具名称、参数、执行耗时、返回结果。当前迭代轮数。这些日志既可以用于排查问题,也可以作为后续评测 Agent 效果的数据集。8.4 安全边界:最小权限原则前面反复强调过,工具执行权在你的代码,不在模型手里。生产环境中,还要落实以下几点:模型生成的参数要经过校验后才能传给工具。涉及删除、修改、发送消息等高风险操作时,建议增加二次确认机制。敏感字段(手机号、邮箱、身份证)在写入日志前要脱敏。工具函数使用独立账号或最小权限密钥,不要用超级管理员权限调用数据库或消息服务。8.5 幂等与重试工具调用可能因为网络超时而失败。重试机制要设计成幂等的,尤其是发送通知、扣减库存这类操作。简单做法是:工具入参加入请求 ID,后端判断该 ID 是否已经处理过。8.6 成本与限流每一轮 Agent 循环都消耗 token。一个复杂任务可能要调用 5 到 8 次工具,对应的模型调用成本会成倍增加。建议在代码里加上 token 数统计和每日限额,并针对高频工具做缓存。如果同一个工具在短时间内被反复调用且参数相同,直接返回缓存结果即可。8.7 测试与评测Agent 的测试不能用传统单元测试的思维,因为输出不是固定的。业界常用的做法是准备一组合法任务和非法任务,构建评测集,然后通过人工或另一个模型给结果打分。每改动一次提示词或工具描述,都要回归跑一遍评测集。没有评测集的 Agent 优化,基本等于盲调。9. 总结与后续学习方向这篇内容没有去罗列各种 Agent 框架的 API,而是从底层带你实现了一个最小闭环。你现在应该能理解几件事:Agent 的核心是“模型决策 工具执行 循环控制”三者的协作;工具注册表和参数描述决定了模型能不能正确使用工具;消息结构错误和上下文失控是生产环境最常见的失败原因。如果接下来想继续深入,建议按顺序研究几个方向:第一,把固定工具列表改成“按需加载”,当任务需要时才把对应工具注入提示词,降低 token 消耗;第二,引入短期记忆压缩,当对话轮次超过阈值时用摘要替代原始消息;第三,给 Agent 增加子任务拆分能力,每个子任务单独跑一个 Agent 循环,最后汇总结果。落到实际项目里,我的建议是先跑通本文的最小实现,再决定要不要引入框架。很多场景下,一百行代码的 Agent 比一套复杂框架更可控。先定义好工具边界和权限边界,再逐步增加记忆、规划和并发能力,这才是 Agent 落地最稳妥的路径。