两个多月前有位读者给我留言“我已经能把大模型API调通了但总觉得自己写的是聊天机器人离AI Agent还差着十万八千里到底差在哪”这个问题最近被问得特别频繁所以我决定用一整篇文章把它讲透。在Python生态里做AI Agent实战开发并不是去套某个神秘框架而是要真正理解一个智能体如何感知环境、调用工具、处理反馈并在这套循环里做出稳定决策。这篇文章适合已经会基本Python、看过一点大模型接口文档、但还没完整搭过智能体的朋友。你会看到一个最小可用的智能体是如何一步步从想法变成可执行代码的。1. 为什么说“能调通大模型API”不等于“拥有智能体”1.1 聊天机器人一直在“等你说下一句”先抛结论很多人对AI Agent的第一印象是“功能更强的聊天机器人”这个类比其实很误导。你调通大模型API之后默认交互形态是请求→回复的问答闭环用户问一句模型答一句然后流程结束等待下一次提问。整个过程里没有目标、没有行动、没有反馈修正。智能体的核心词根是“agent”直译就是“能主动做事的行为体”。它至少要有三个区别于聊天机器人的特征有目标、能行动、会迭代。目标决定它要达成什么行动决定它调什么工具、拿什么数据迭代决定它拿到结果之后是否继续思考、纠正、逼近目标。用一个生活类比来解释聊天机器人像一台自动售货机你投币选饮料它吐一罐出来交互到此结束。智能体像外卖配送调度员你告诉他“下午三点前把货送到”他不会原地等你追问而是自己查库存、看路线、评估天气、选择配送方式然后完成派单。如果路上出现异常他还会主动调整方案。这个“自动售货机”和“配送调度员”之间的差距就是Agent开发真正要做的事。1.2 几个高频名词的边界先厘清再说我经常在社区看到有人把Agent、工作流、RAG这几个概念混着用这里顺手梳理一下概念核心特征典型形态大模型文本生成模型本身不做决策也不调工具文本输入输出工作流人预先定义好的固定步骤模型只做其中一环规则引擎模型RAG从外部知识库检索信息后再交给模型生成回答文档问答Agent模型自主规划步骤、调用工具、观察结果、迭代推进工具调用循环Agent和工作流最核心的区别在于“决策权放在哪里”工作流的走法在代码里写死Agent的走法由模型每一步自主决定。真实项目里两者经常混用固定流程用工作流需要随机应变的部分交给Agent。另外补充一个认知Agent并不是新理论“感知-决策-行动”的闭环在传统人工智能和控制论里存在了几十年大模型的加入只是让“决策”这一步从规则判断升级成了自然语言推理普通开发者才真正接得住。1.3 从“先想后做”到“边想边做”早期Agent实现常采用“先规划后执行”的流程Agent第一步把任务拆成子任务清单第二步按清单逐项调用工具最后汇总。这种模式的问题是一旦执行中发现规划不合理重头再来成本很高。目前更主流的是ReAct模式也就是推理与行动交叉进行。每一步模型先写一段思考比如“用户要算送达时间我需要先查出订单发出时间”然后发起一次工具调用拿到结果之后再思考下一步。翻译成人话就是Agent不再等到把所有事情都想清楚才动手而是边想边做做一步看一步。ReAct之所以流行是因为现实任务很少能一次性规划到位。比如用户问“这个订单什么时候到”你需要先知道订单号再知道从哪里发出、发往哪里然后才能估算时效。中间任何一个环节信息缺失Agent都要临时补一个动作去查。这种动态补缺的能力是问答式API给不了你的。2. 选好第一个场景比学会框架更重要2.1 为什么我不建议你直接写“万能个人助理”很多人的第一个Agent想法是“像市面上主流助手一样什么都干”。愿望很好但作为第一个练手项目几乎必崩。原因很简单万能助理面对的是开放域问题要同时覆盖聊天、搜索、文件操作、日程管理、邮件、提醒等十几个能力域每个域的工具都要处理权限控制、状态管理和错误恢复工程量一开始就是地狱级。更合理的路径是先做一个“垂直小助手”边界清晰、术语固定、动作种类少、失败影响可控。我帮人选场景时常用两个标准第一任务的输入输出是不是结构化的订单号、时间、地点这类信息比“帮我写篇文章”好处理得多第二能不能明确判断任务成没成功有明确答案比开放式回答好验证得多。2.2 候选场景对比为什么最后选了物流调度助手我准备过几个候选场景做对比也顺便展示一下选型时真正应该关注什么候选场景工具复杂度验证难度翻车成本综合推荐文章摘要助手低中低适合热身数据分析问答中中中适合进阶物流调度助手中高低模拟数据适合第一个完整项目智能日程管理高低高会误发消息不适合入门最终我选的是模拟环境里的“调度助手Demo”。它接收类似这样的请求“订单SO-20250321-002预计几点到城市乙如果不是很急能不能不安排加急”这类任务需要Agent查订单、查路线时效、判断优先级再给出调度建议既要调用多个工具又要组合信息做推理但所有数据都是模拟的写错了也不会造成真实损失。2.3 把目标压缩成一句话后面所有代码才有方向动手写代码之前我用一句话定义了Demo目标输入一段自然语言物流问题Agent通过调用三个内部工具获取真实数据基于数据推理并输出可执行结论若信息不足必须主动追问绝不编造。这句话锁定了四个约束一是工具范围固定只有三个二是回答必须基于工具返回值三是允许发起追问四是禁止编造。“禁止编造”这一点尤其重要它决定了系统提示词怎么写、主循环要不要加校验步骤。这也是为什么我说先压缩目标再写代码比先装环境再想做什么要高效得多。如果目标不能用一句话说清楚说明场景还没选明白。3. Agent运行机制拆解工具、记忆与循环如何协同3.1 工具层函数调用到底是怎么运作的工具层是智能体的手脚。在这个Demo里工具就是三个普通Python函数。问题在于模型怎么知道用户想调哪一个答案是函数调用机制不同平台叫法略有差异本质一致。你先把每个函数的名字、参数、描述按约定的JSON Schema格式发给大模型模型就会在输出里返回一个“想要调用某函数”的结构化结果。它不会真的替你执行。这里必须澄清一个常见误解大模型不会在你的进程里执行函数它只是“推荐一个函数调用”。真正执行的是你的代码。主循环拿到模型输出的函数名和参数后自己调用Python函数再把返回值以消息形式发回给模型。模型看到返回值之后才能结合原始问题组织最终回答。关于工具描述写多细我的经验是参数说明一定要给示例值否则模型很容易传错格式。比如“出发时间”只写“depart_time: 出发时间”模型可能传“下午1点”而不是“13:00”如果写上“24小时制字符串示例13:00”准确率会明显提升。3.2 记忆层短期会话与长期召回的分工记忆层回答的是“Agent还记不记得之前聊了什么”。最简单的是短期记忆把用户问题、模型回复、工具结果全放进消息数组每次都整体传给大模型相当于让模型重新读一遍聊天记录。代价是token消耗随轮数线性增长对话一长成本很可观。长期记忆则是把有用的历史信息抽出来存成外部索引最常用的是向量数据库。下次用户带来类似问题时主动召回相关内容作为上下文的一部分。这个Demo初级阶段用的是短期记忆加一个小的人工缓存到后面才扩展成长记忆。新手不要一上来就上向量库先把消息数组玩明白理解“上下文长度就是记忆边界”再谈持久化。3.3 主循环让Agent“边想边做”的裁判机制把所有组件串起来的是一个几十行的循环逻辑上只有四步发请求、看回应、执行工具、反馈回模型。伪代码如下把系统提示词、用户问题和历史记录拼成消息数组发给大模型带上工具Schema判断模型返回内容里有没有工具调用请求有工具调用 → 执行对应函数 → 把结果加进消息数组 → 回到第2步没有工具调用 → 把最终内容返回给用户结束循环这段流程对应着ReAct模式模型每次的思考留在消息数组里工具的观察结果也留在消息数组里循环就是“边想边做”的实现载体。这里最容易出问题的不是单步逻辑而是循环终止条件如果模型一直想调用工具、迟迟不产出最终答案代码会无限跑下去。所以主循环必须设最大步数上限。3.4 框架与手写Agent编排脚手架到底帮了什么初学者最容易问直接用现成的Agent编排框架不是更快吗我的看法是框架适合生产项目里快速整合模块但不太适合学习第一个Agent。框架把工具调用、记忆管理、执行器逻辑都封装在黑盒里一旦出问题你很难判断是模型输出错、工具参数错、还是框架缓存错。我的实践顺序是先用原生API手写一轮主循环跑通后再去看框架源码里对应模块这时你能很快看懂它封装的原因最后再决定要不要引入框架。跳过手写直接上框架等于学开车直接开自动驾驶翻车了都不知道该查哪个部件。4. 从0到1实现你的第一个智能体物流调度助手4.1 环境准备与依赖安装我用的是Python 3.113.10到3.12之间都问题不大别用太旧的版本。建议创建虚拟环境再装依赖python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 安装你选用的模型平台SDKllm_sdk为占位按实际平台文档替换包名 pip install llm_sdk python-dotenvllm_sdk只是占位符号请你按实际选择的大模型平台文档安装对应SDK。真正要紧的是这个前提所选模型必须支持函数调用或工具调用能力否则后面的机制无从谈起。python-dotenv用来读取.env配置文件把API密钥写在环境变量里不硬编码进源码。.env文件大致这样API_KEYyour_api_key_here BASE_URLhttps://your-compatible-endpoint MODEL_NAMEyour-model-name再写一个config.py统一加载配置。密钥绝对不能硬编码这是基础习惯但我见过太多次因为硬编码导致泄露的案例这里必须强调。4.2 工具层三个最朴素的函数工具层是整个项目里最不能被框架绑架的部分Agent手里的每一个函数本质都是你业务能力的映射。我这里写了三个函数query_order查订单、calc_eta算时效、gen_schedule生成调度建议。# tools.py from datetime import datetime, timedelta ORDER_DB { SO-20250321-001: {from_city: 仓A, to_city: 城市甲, depart_time: 09:30, priority: normal}, SO-20250321-002: {from_city: 仓A, to_city: 城市乙, depart_time: 13:00, priority: high}, SO-20250321-003: {from_city: 仓B, to_city: 城市甲, depart_time: 10:00, priority: normal}, } def query_order(order_id: str) - dict: 按订单号查询订单基础信息 return ORDER_DB.get(order_id, {error: f未找到订单 {order_id}}) def calc_eta(from_city: str, to_city: str, depart_time: str) - dict: 根据出发地、目的地、出发时间估算送达时间 route_hours {仓A_城市甲: 4, 仓A_城市乙: 6, 仓B_城市甲: 3, 仓B_城市乙: 5} key f{from_city}_{to_city} hours route_hours.get(key, 5) eta datetime.strptime(depart_time, %H:%M) timedelta(hourshours) return {route: key, hours: hours, eta: eta.strftime(%H:%M)} def gen_schedule(order_id: str) - dict: 根据订单信息生成调度安排 info query_order(order_id) if error in info: return {error: f无法生成调度安排{info[error]}} schedule (f订单{order_id}由{info[from_city]}在{info[depart_time]}发出 f运往{info[to_city]}优先级{info[priority]} f送达后完成入库) return {schedule: schedule}这三个函数刻意写得很朴素它的返回结构是dict因为结构化结果会让模型解析起来更稳定。错误信息写得明确比如“未找到订单xxx”模型看到这个反馈就能决定下一步是改参数重试还是直接告知用户。4.3 工具注册表与模型调用封装接下来把工具暴露给大模型。我需要一个字典完成函数名到函数对象的映射用于执行还需要一个符合平台要求的工具Schema列表用于告诉模型有哪些函数可调。# agent_core.py import json from tools import query_order, calc_eta, gen_schedule TOOL_MAP { query_order: query_order, calc_eta: calc_eta, gen_schedule: gen_schedule, } TOOL_SCHEMAS [ { type: function, function: { name: query_order, description: 查询订单基础信息, parameters: { type: object, properties: { order_id: { type: string, description: 订单号示例SO-20250321-001 } }, required: [order_id] } } }, { type: function, function: { name: calc_eta, description: 估算送达时间, parameters: { type: object, properties: { from_city: {type: string, description: 出发仓示例仓A}, to_city: {type: string, description: 到达城市示例城市甲}, depart_time: {type: string, description: 出发时间24小时制字符串示例13:00} }, required: [from_city, to_city, depart_time] } } }, { type: function, function: { name: gen_schedule, description: 基于订单信息生成调度安排, parameters: { type: object, properties: { order_id: { type: string, description: 订单号示例SO-20250321-001 } }, required: [order_id] } } } ]写参数描述时我格外强调示例值。模型很容易把“下午1点”传成“1:00 PM”或“13点”Schema给得越具体后面调试越省事。TOOL_MAP负责把模型推荐的函数名转成真实调用执行部分则由主循环处理。模型调用封装我写成一个llm_chat函数把消息数组和工具列表传给接口temperature固定为0.2。Agent不是创意写作不需要发散性温度低一点能让结果更稳定。# config.py import os from dotenv import load_dotenv load_dotenv() # llm_sdk 为占位按实际选择的大模型平台替换导入方式 from llm_sdk import Client client Client(api_keyos.getenv(API_KEY), base_urlos.getenv(BASE_URL)) def llm_chat(messages, toolsNone): resp client.chat.completions.create( modelos.getenv(MODEL_NAME), messagesmessages, toolstools or [], temperature0.2, ) return resp.choices[0].message4.4 主循环最精简的Agent骨架主循环是整个Demo的发动机它保证“模型提需求、代码执行、结果反馈、模型再思考”这个闭环正确运转def run_agent(user_input: str, max_steps: int 5): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for step in range(max_steps): response llm_chat(messagesmessages, toolsTOOL_SCHEMAS) messages.append(response) tool_calls getattr(response, tool_calls, None) if not tool_calls: return response.content for call in tool_calls: fn_name call.function.name args json.loads(call.function.arguments) print(f[step-{step}] call {fn_name}({args})) result TOOL_MAP[fn_name](**args) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse), }) return 已超过最大执行步数停止运行。请简化需求或补充信息后重试。这段代码是整个Demo里最短的部分但信息量很大。for循环天然限制了最大步数这是防失控的第一道保险。每次拿到模型响应先检查有没有工具调用请求没有就说明模型准备直接回答可以结束。有就执行执行结果以tool角色消息回到消息数组让模型看到观察结果。中间那行print是刻意留下的调试口子控制台能看到Agent每一步在做什么、调了哪个函数。别小看它下面的排查工作全靠它。4.5 提示词决定Agent像不像样的第一要素主循环只是骨架系统提示词才是“性格”。我给的提示词长这样SYSTEM_PROMPT 你是物流调度助手。你的任务是根据用户问题逐步调用可用工具获取真实信息再给出结论。 可用工具 - query_order(order_id)查询订单信息返回订单的发出仓、目的地、出发时间、优先级 - calc_eta(from_city, to_city, depart_time)估算送达时间depart_time使用24小时制字符串如13:00 - gen_schedule(order_id)基于订单信息生成调度安排 工作规则 1. 回答前必须先用工具确认信息禁止编造订单号、时间或路线 2. 一次只调用一个工具观察结果后再决定下一步 3. 获取足够信息后立即用一句话给出结论不要重复调用工具 4. 如果工具返回未找到订单应坦诚告知用户并提示提供正确订单号 提示词写法跟写接口文档很像告诉模型你有什么、怎么用、边界在哪。四条规则都不是摆设第一条压制幻觉第二条让行为可控第三条给循环一个明确的出口第四条规定了错误处理方式。4.6 端到端跑通一个任务体系搭好之后拿一个真实输入测试“订单SO-20250321-003什么时候能送到城市甲着急吗”调用run_agent之后控制台会打出类似这样的调用链[step-0] call query_order({order_id: SO-20250321-003}) [step-1] call calc_eta({from_city: 仓B, to_city: 城市甲, depart_time: 10:00})最终模型输出类似“订单SO-20250321-003从仓B开往城市甲10:00发出预计13:00送达优先级正常不算最急。”这说明Agent完成了完整闭环先查订单拿基础信息再测算时效最后基于数据回答。如果用户输入缺了必要信息比如“帮我算下某订单什么时候到”Agent在query_order查不到订单后会根据错误信息主动追问“请提供正确的订单号”而不是编一个答案。这正是前面强调的“信息不足必须追问”原则在起作用。5. 把任务跑顺并不等于跑对测试阶段最典型的4次翻车5.1 翻车一Agent无限循环调同一个函数第一个版本的提示词没有写“获取足够信息后立即给结论”结果遇到含糊任务时控制台出现一连串calc_eta调用参数几乎一样像卡了带一样重复执行。原因很快定位模型每次拿到相同的工具结果如果上下文里没有“可以结束”的明确信号它就倾向于“再确认一次”。修复分两处第一在提示词第三条明确写出“获取足够信息后立即给出结论不要重复调用工具”第二主循环的max_steps设置为5双重保险。复盘时我发现这类问题几乎都源于提示词没给模型“出口暗示”。模型不是不想停是你没告诉它什么时候可以停。5.2 翻车二工具参数传错根因在Schema描述另一个高频翻车是参数错乱。有一次调试模型调用calc_eta时把出发时间传成了“下午1点”datetime解析直接抛异常。我当时第一反应是模型不行拉出日志才发现是Schema描述写得模糊只写了“出发时间”模型当然按人类语言习惯传值。修复方式是Schema描述里明确“24小时制字符串示例13:00”并在工具函数入口加一层异常捕获解析失败时返回“出发时间格式应为HH:MM”。模型看到这个错误反馈会自动修正参数重试。这个经验后来我复用到所有工具上描述里给示例比任何提示词规则都管用。5.3 翻车三对话长了之后Agent忘了最初目标测试连续提问时发现了记忆衰减问题用户先问“只关心今天的订单”几轮之后Agent就开始计算明天的单子因为上下文里“今天的订单”这个约束被后续对话冲淡了。这是短期记忆的天然短板模型对越靠后的内容权重越高。第一轮修复是在每轮对话末尾把当前核心约束回写成“当前目标”消息重新塞进上下文相当于给模型贴了张便利贴。要更彻底的话就得引入外部长期记忆把用户偏好存成索引在下一个任务开始前主动召回。5.4 翻车四模型“理直气壮”地编造工具结果最危险的一次翻车是幻觉我问“订单SO-20250321-999在哪”Agent在工具返回“未找到订单”之后仍然煞有其事地回答“该订单正从仓A运往城市甲预计下午3点送达”。这完全是编造而且编得很流畅自然。我检查调用链后发现提示词第一条虽然写了禁止编造但模型依然可能在被工具结果打脸时强行圆场。修复方向是细化错误处理规则“如果工具返回未找到订单直接告诉用户未找到并请提供正确订单号禁止补充任何额外信息。”同时我在主循环里增加了后置判断只要最终回答中不含任何工具返回的关键信息比如订单号、时间就输出“异常终止建议重新提问”。虽然简单但能拦下一部分幻觉。这套思路后来被我总结成“回答必须能溯源”原则语言可以自由组织但每个事实性结论都要能被某次工具结果证明。这是Agent从“会聊天”到“办正事”的分水岭。5.5 调试三件套打印调用链、模拟工具、监控成本最后把调试经验收拢成三件事主循环里那行print是开发期的生命线正式环境可以换成结构化日志但开发期必须保留。给每个工具写单元测试把合法和非法输入都测一遍。遇到问题先判断是模型决策错还是工具实现错别一上来就怪模型。统计每轮消耗的token和调用次数给单次会话设置成本上限。等月底账单出来再意识到失控那就晚了。6. 进阶这就是智能体真正落地的分水岭6.1 长期记忆让Agent记住“这个用户总问哪条线”小Demo跑通之后第一件值得做的事是加长期记忆。最简单的方案把每次会话结束时的关键信息比如用户常问线路、常用仓、偏好提取成短文本向量化存入本地索引。下一次会话开始时用当前问题做一次相似度搜索召回最相关的几条历史记录拼进系统提示词。在模拟项目X里效果很明显用户第二次来问同一线路时效时Agent已经能说出“您上次问过仓A到城市乙这次同样按6小时估算”体验感和一次性聊天完全不同。但长期记忆也会带来新问题召回不准会误导Agent隐私数据不能随便存。起步时建议先做按用户ID隔离的轻量记忆不要做全局共享记忆。6.2 多Agent协作不要把希望压在单个模型身上把一个大任务拆给多个Agent是另一个进阶方向。比如拆成“需求理解Agent”和“调度决策Agent”前者把用户口语化问题转成结构化需求后者只负责基于结构化需求优化排班。两个Agent之间通过JSON协议通信而不是自由对话。这里最核心的工程经验是Agent之间的通信协议越简单越好。如果你发现两个Agent在来回传递一大段自然语言说明拆分粒度不对应该把自然语言交互限定在面向用户的那一端内部全部走结构化消息。多Agent的收益来自单一职责加清晰接口不是模型数量堆出来的智能。6.3 回归测试集改一次提示词把20个老问题重跑一遍做Agent开发最痛苦的是改提示词导致“修好一个旧问题、弄坏三个老功能”。我后来强制自己维护了一个20条问题的回归集每次改完提示词或工具逻辑就把这20条全部跑一遍统计通过率。虽然全量跑要花十几分钟但这十几分钟挡住了大量线上回归事故。回归集里的问题要刻意覆盖三类正常输入、缺参数输入、错误输入。通过标准不能是“回答内容差不多”而要检查关键字段是否出现在最终回答里。如果要求答案能溯源就让脚本自动比对回答里的订单号、时间值与工具返回是否一致。6.4 落地前最后一道闸人工确认与权限最小化如果这个Demo要接真实业务我建议至少加两道闸。第一道是高风险动作确认机制gen_schedule这类会产生实际业务影响的动作工具执行后不要直接返回结果而是返回“待确认状态”由用户点击确认后才真正落地。第二道是权限最小化Agent能调用的工具必须走白名单绝不能让它拿到一个自由执行的Shell或万能HTTP客户端否则一次提示词注入就能酿成大事故。此外还要做输入长度限制和敏感信息过滤日志里避免记录真实身份信息。这些听起来跟Agent没关系但真实项目上线后被讨论最多的往往是这部分。最后分享一点个人体会。我在跑通第一个智能体之后很长时间都在反复想一个问题为什么同样的模型、同样的工具有的人写出来像老练的业务员有的人写出来像复读机答案慢慢落在这三件事上工具是否干净可靠、提示词是否把边界说透、主循环是否设好了止损条件。这三件事没有一件需要高深理论却决定了Agent从“能跑”到“好用”的全部距离。如果你也正在搭自己的第一个智能体我的建议是挑一个两小时能做完的垂直任务把主循环手写一遍然后认真写十条测试问题。做完这些你对Agent的理解会比看十篇框架文档深得多。
阅读完成 · 觉得有帮助?