
在把第一个真正意义上的 AI Agent 跑起来之前我对“Agent”这个词一直持谨慎态度。市场上很多演示停留在“能聊天”可我真正想要的是让程序自己决定下一步该做什么——需要查时间就查时间需要算数据就算数据而不是把所有逻辑都写在 if-else 里。后来我试着用一套 Agent SDK 把最小闭环跑通突然发现之前觉得复杂的事情其实可以被框架摊得很平。这篇文章就是这次实战的记录内容包括环境搭建、第一个 Agent 怎么写、工具调用机制、运行参数调整以及上线前必须注意的权限和成本问题。希望读完之后你也能在 5 分钟左右拥有自己的第一个 AI Agent。1. Agent 和“调用 API 聊天”根本不是一回事1.1 模型只能输出文本但 Agent 需要输出动作把对话模型接入业务系统时你很快会遇到一个尴尬事实模型接口返回的是纯文本它本身不会帮你查询数据也不会触发任何副作用。想要一个聊天机器人帮忙查天气你得自己写代码判断用户意图再从文本里抽参数最后调天气接口。功能少的时候还行功能一多整段代码就会变成一堆分支判断改起来非常痛苦。Agent 的思路恰好相反。模型不只是输出一段回答而是输出一个“动作意图”。比如用户说“帮我看看北京现在多少度”模型可能返回一个结构化的工具调用请求里面写明要调用的函数名和参数。你的程序负责真正执行这个函数再把执行结果发回给模型让模型继续推理直到给出最终结论。这样模型就从“话痨”变成了“调度员”这也是 Tom 常说的“从能说到能干”的关键一步。1.2 Agent SDK 帮你省掉的是哪些脏活自己从头实现这套机制不是做不到但要把三件事同时做对才会体感良好。第一是循环管理。模型可能要连续调用多轮工具才能得出答案第一次调用结果会影响第二次调用什么这个循环要有人来维护。第二是协议转换。模型接口并不理解 Python 函数它只认 JSON 形式的工具描述你要把函数签名、参数说明、返回值格式全部变成它能读懂的声明。第三是错误处理。工具执行可能失败异常信息必须回传给模型让模型决定是换一种参数重试还是直接向用户说明失败原因。Agent SDK 一般把这三点封装成了现成的运行时。你只需要定义好工具函数把它们挂到 agent 上然后调用一次 run 方法剩下的轮次控制、结果回填、异常传递都由框架完成。这也是为什么 5 分钟能跑起来真正复杂的部分其实已经被框架消化了。1.3 这套机制适合谁用我觉得只要满足下面任一条件都值得花一顿饭的时间试一下。后端开发者想把自然语言查询接进自己的数据接口。需要做自动化流程但传统规则引擎越来越难维护复杂分支。AI 产品开发者想在 Demo 里快速验证“工具调用”这个核心体验。对 Agent 概念熟悉但还没亲手跑通过一个最小实现。当然它并不适合所有场景。如果你的需求只是固定触发一个接口那定时任务加几行代码反而更可靠。Agent 的核心价值在于“不确定调用链”依赖模型做决策模型一旦给出错误判断反而不如规则稳定。2. 开工前的准备虚拟环境、依赖、API Key 和目录结构2.1 Python 版本和虚拟环境先说版本。以我常用的 Agent SDK 为例它对 Python 版本有最低要求建议直接用 3.10 以上。老版本在类型注解和 async 支持上有不少历史包袱遇到莫名报错时往往查到最后是解释器太老很浪费时间。安装依赖前先建一个干净的虚拟环境。这一步不麻烦但它能避免你后面把系统 Python 环境搞得一团糟尤其是可能和其他项目依赖冲突的时候。python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install agent-sdk python-dotenv如果你用的是 macOS 或 Linux激活虚拟环境后命令行前面会出现一个(.venv)前缀。看到这个前缀就说明后续安装的包不会污染全局环境。Windows 用户记得用 PowerShell 支持的命令不要拿 cmd 的语法硬套。2.2 API Key 别写进源码很多第一次跑通的 Demo 习惯把密钥直接写在代码里这在我眼里是大忌。一旦代码被提交到仓库、截图发到群里密钥就等于泄露了。正确做法是放环境变量。在项目根目录创建.env文件AGENT_API_KEYsk-xxxxx然后在main.py顶部加载它import os from dotenv import load_dotenv load_dotenv() api_key os.getenv(AGENT_API_KEY)如果你用的是 Git 管理代码记得在.gitignore里加上.env。这个文件是本地私密信息不该出现在任何版本历史里。2.3 最小目录结构第一次做 Agent不需要把项目拆得多复杂。我会建议保持一个最小的结构方便随时删掉重来demo_agent/ ├── .env ├── .gitignore ├── main.py └── requirements.txtmain.py是我们的入口工具函数也可以直接先写在同一个文件里。等工具多到五十行以上再拆成tools.py也不迟。过早抽象是新手最容易犯的毛病Agent 项目尤其如此因为工具之间依赖关系还没稳定拆得太早后面重构更累。3. 第一个 Agent让它学会在需要时调用工具3.1 核心代码只有几十行下面这段代码就是最小可运行版本。我故意只做了一个工具函数作用是返回当前时间然后让 Agent 自己去判断该不该调用它。from datetime import datetime import os from dotenv import load_dotenv from agent_sdk import Agent from agent_sdk.tools import FunctionTool load_dotenv() def get_current_time() - str: 获取当前时间的字符串格式。 now datetime.now() return f当前时间是 {now:%Y-%m-%d %H:%M:%S} time_tool FunctionTool.from_function(get_current_time) agent Agent( api_keyos.getenv(AGENT_API_KEY), tools[time_tool], system_prompt你是一个效率助手。当用户需要时间信息时调用 get_current_time 工具获取其他问题直接回答。, ) result agent.run(请查一下当前时间然后告诉我现在适合做什么准备工作。) print(result.output)跑起来的时候你会在控制台看到类似“正在调用工具 get_current_time”的日志。这说明模型确实在按我们的要求行动而不是单纯从训练记忆里瞎编时间。这是非常关键的一步原本靠猜的信息开始通过真实函数获得。3.2 Agent.run() 内部发生了什么很多人看完代码会问agent.run()到底做了什么以这段代码举例内部大致是这么走的SDK 把get_current_time的函数签名、参数说明、返回值说明转换成模型能识别的工具定义。把用户提问、系统提示词和工具定义一起发给模型。模型判断自己需要时间信息返回一个工具调用请求。SDK 执行get_current_time()拿到字符串。工具执行结果作为一条新消息回传给模型。模型看到结果后生成最终回答“现在是几点适合做什么准备”。循环结束返回结果。如果你认真数一数会发现用户只发起一次请求但底层模型未必只被调用一次。可能在工具结果返回后还要再生成一次。这也是 Agent 比普通聊天耗时更长的原因。3.3 为什么最大轮次 max_turns 越早设越好假设你忘记设置max_turns而模型又不停调用工具程序就会陷入一种很像“死循环”的状态。SDK 一般会给一个较大的默认值但你不该依赖它。建议在Agent构造时显式传入agent Agent( api_keyos.getenv(AGENT_API_KEY), tools[time_tool], max_turns8, )8 听起来不多但对大多数“查询信息-整理回答”的任务足够了。多轮工具调用会消耗更多 token还会拉长响应时间所以设个上限既是保护也是倒逼自己把工具设计得更直接。如果某个任务经常需要超过 8 轮才能完成说明工具边界设计有问题而不是上限太高。4. 工具注册与调用比想象中更严格参数声明和错误恢复4.1 函数签名就是模型的操作手册把普通函数包装成工具时函数名、参数名、类型注解、文档字符串都会成为模型的“操作手册”。这段话我必须强调文档字符串写得好不好直接影响模型调用正确率。看一个反例def get_weather(city: str, unit: str celsius) - str: 查询天气。 return f{city} 的天气是晴天温度 25 度单位 {unit}这个工具描述里没有说明unit参数允许哪些值。模型虽然知道有unit这个东西但很可能传一个度或temperature进去导致你代码解析出错。好一点的写法是def get_weather(city: str, unit: str celsius) - str: 查询指定城市的实时天气。 city: 城市名例如 北京、上海。 unit: 温度单位可选 celsius 或 fahrenheit默认 celsius。 # 实际逻辑略 return 晴天 25参数说明越具体模型越不容易放飞自我。Agent 的工具有点像给新人使用的工具清单写清楚使用条件才是负责任的。4.2 工具返回结果别只丢一个 “ok”另一个常见问题是工具返回值太精简。函数执行完只返回一个ok模型拿不到任何可用信息只能继续追问或编造。正确做法是返回结构化信息甚至直接序列化成 JSON。import json def get_weather(city: str, unit: str celsius) - str: data { city: city, weather: sunny, temperature: 25, unit: unit, updated_at: 2025-01-01 10:00:00, } return json.dumps(data, ensure_asciiFalse)模型收到 JSON 后能直接把里面的温度、城市名、更新时间组合成自然语言回答而不是靠猜。试想一下如果工具返回25模型连单位都无法确定错误输出几乎是必然的。4.3 工具执行出错别急着让整个任务失败Agent 工具运行出现异常是日常而不意外。函数可能拿到非法参数可能访问外部接口超时这些都是正常现象。关键是怎么处理。我见过不少项目在工具函数里用 try-except 把异常吞掉然后返回一个error。这其实是在浪费一次模型调用机会。更好的做法是返回一段人能读懂的、说明失败原因的字符串比如查询超时天气服务 5 秒内未响应请稍后重试。模型拿到这段信息后会主动修正策略可能是换个城市简称再试也可能是直接告诉用户“服务暂时不可用”。这才是 Agent 的价值所在。下面这张表总结了常见的失败场景和模型可能的应对方式失败场景工具返回值风格Agent 可能采取的策略参数格式错误返回可读错误信息说明期望格式修正参数后重试调用外部接口超时返回具体超时提示改为询问用户是否稍后重试权限被拒绝返回“无权限”而不是堆栈信息终止该工具调用改用其他方案数据为空返回“未找到结果”和可能原因换个关键词或向用户说明无结果当然连续重试也会耗尽 max_turns。所以工具出错时一定要把信息写得足够清楚否则模型会在原地打转。5. 从演示到能用我首先调整的是这四个运行参数5.1 max_turns不是越大越好前面说过max_turns控制模型能调用多少轮工具。超过阈值后SDK 会停止执行并返回一个“已达上限”的结果。实际经验是大多数查询类任务的合理轮次在 6 到 10 之间。如果发现任务经常需要高分轮次才能完成不要直接调大参数先检查工具是不是太碎片化了。比如你要查一个“电商订单状态”可能需要模型依次查订单接口、物流接口、优惠券接口三四个工具调用正常。但如果一个动作拆成十个工具例如把“查询商品价格”“查询商品库存”“查询商品评分”分开那模型就要多走好几轮既慢又费钱。5.2 timeout单次请求超时别设太短Agent 和普通接口调用不一样它的单次 run 时间可能包含多轮模型推理和工具执行。很多初学者容易把超时时间设成 5 秒、10 秒结果模型还没返回第一步就超时中断了。我的建议是把timeout设置在 30 秒到 60 秒之间。第一次跑一个未知 Agent 时宁可多等也不要轻易把超时调短。等后面确定任务复杂度再逐步下调。给每个工具单独设置超时是个更精细的做法但这会加重代码复杂度建议留到项目成熟后再做。5.3 并行工具调用能省时间但也有前提部分 SDK 支持在同一轮同时执行多个工具也就是模型一次性返回多个 tool_call。这样可以把三个没有依赖关系的外呼接口并行执行响应时间会明显缩短。但并行不是没有代价。首先工具之间不能有数据依赖如果工具 B 需要工具 A 的结果那并行就是灾难。其次并发调用会同时消耗多份资源增加限流风险。建议先确认 SDK 的并行参数名在 demo 阶段用关闭状态起步等工具稳定了再试点打开 2-4 个并行。5.4 上下文窗口和成本控制每多一轮就多一笔钱Agent 消耗 token 的规律其实很直观每一轮模型推理都要把工具定义、历史对话、工具结果重新放入上下文。工具定义越长、调用轮次越多开销就越大。一个简单估算方法是假设一次纯聊天回答消耗 1000 token如果 Agent 完成了 3 轮工具调用总消耗可能接近 2500 到 3000 token。这是因为每一轮都会带上前面的历史导致越到后面单轮请求越大。控制成本我常用的几个手段工具描述尽量精简删除冗余文档字符串。系统提示词只保留必要的约束不要把完整规划塞进去。关闭不需要的工具比如临时把查询类工具全部移除。对成本敏感的批量任务设置单次 run 的 max_tokens 上限避免模型回答过长。6. 权限和观测Agent 工具不是“随便跑一段函数”6.1 给模型的工具要有边界让模型有能力调用函数本质上是把系统的一部分控制权交给了一个模型。虽然模型不一定有恶意但出错时的破坏力可能很大。我在生产环境中碰到过一个案例因为工具描述写得不清楚模型错误地调用了“删除用户”接口好在对方接口有二次确认才没酿成事故。给你的函数工具设置四道底线不要直接暴露os.system、subprocess.run这类可以执行任意命令的接口。工具函数内部自己做输入校验不要依赖模型传参的合理性。涉及删除、修改、发送等危险操作工具内部必须强制二次确认或加白名单。不要让工具返回完整异常堆栈避免把内部路径和系统信息暴露给用户。有一个比较安全的做法是把 Agent 放在低权限的容器或沙箱里运行这样即使模型调了某个有问题的工具影响范围也有限。6.2 日志和可观测性没有日志就没有排查依据Agent 的调用链不是固定顺序模型每一步选择都不同这比传统接口排查难度高很多。所以从第一天开始我建议开发时就打印完整工具调用链。至少要知道这几件事模型在每一轮选择调用哪个工具。传入的工具参数是什么。工具执行结果是什么。单轮耗时和总耗时。是否发生重试重试了几次。很多 SDK 会提供回调钩子例如在每一步调用结束时执行一个自定义函数。你可以把信息写入日志系统之后就算 Agent 在线上表现出奇怪行为也能回头复盘。6.3 版本固定与可复现性Agent 项目比普通后端项目更容易“玄学”。同一条用户消息换了模型版本后可能给出完全不同的答案。为了让项目可复现最好把依赖版本锁死。requirements.txt里不要写宽容范围agent-sdk0.8.3 python-dotenv1.0.1同时尽可能在代码里固定模型名称和温度参数。把温度设为 0 会显著提高工具调用的稳定性减少模型随机发挥。当然0 不是绝对某些创意类场景还是需要一点随机性但工具调用场景我强烈建议低温。7. 最后一句大实话只有形成闭环才算第一个 Agent如果把这块文章浓缩成一条经验我的建议是不要一上来就规划一个复杂的 Agent。先做一个只有一个工具的最小闭环观察日志里模型如何从理解请求到调用函数再到拿着函数结果生成最终回答。这段体验比任何概念讲解都有价值。我见过不少开发者第一次尝试就急着同时接入五六种工具还给 Agent 安排一堆前置条件结果调试时根本分不清是工具问题还是模型理解问题。真正应该做的是让这个只有一个时间工具的例子先稳定跑 10 次确保请求、调用、回传、输出全链路顺畅再逐渐增加第二个、第三个工具。顺着这个做法扩展下去你可以试着再加一个天气查询工具让模型在用户问“要不要带伞”时同时调用天气工具和时间工具。也可以把工具调用的结果保存到数据库完成一个简单但完整的自动化任务。当你能熟练控制 max_turns、并发参数、工具错误回传和日志观测时第一个 Agent 就不再是玩具而是一个可演化的自动化基础。到那时候你会发现5 分钟跑通只是入口真正有意思的是后面一步一步把它打磨成顺手工具的过程。