
智能体AI Agent这个说法过去一年几乎被说烂了但真要我一句话讲清楚它和普通对话框的区别我会说对话框是你问一句它答一句智能体是你给它一个目标它自己拆任务、调工具、看结果、改方案直到把事情办完。Python AI 智能体编程说白了就是把这套“拆解-执行-反馈-调整”的循环用代码固化下来。这篇是实用指南的第一篇不铺概念直接从最小可运行的智能体结构讲起适合已经会写 Python、正在琢磨怎么从“调用大模型接口”跨到“做真正能干活的应用”的开发者。看完你会得到一个能跑的主循环骨架以及一堆我在实测里踩出来的坑。1. 先想清楚智能体到底比普通对话框多做了什么1.1 从“问答”到“干活”的跨越普通对话式应用的信息流是单向的用户输入模型输出文本。模型再聪明也只能“说”不能“做”。它查不了数据库调不了内部接口算不了实时数据改不了配置文件。你问它“帮我查一下订单号 ORD-2025-001 的状态”它要么瞎编一个状态要么给你一段怎么手工查询的教程——因为它根本没有查询订单系统的能力。智能体的本质改变在于把“说话”和“行动”两个环节接在了一起。模型在生成回答时可以额外输出一个结构化指令告诉你的代码“去调用某某工具参数是某某”你的代码负责真正执行再把执行结果以消息形式喂回给模型让模型基于真实结果继续推理。这个来回过程就是智能体和对话框最本质的分水岭。很多初学者把智能体理解成“给模型加个插件”这个理解只对了一半。插件解决的是“模型能访问什么”而智能体解决的是“模型如何围绕目标自主行动”。后者才是工程难点什么时候该调工具、调完怎么判断结果、失败了怎么办、循环几次就该收手这些问题都得由代码来兜底。1.2 智能体的最小循环感知-决策-行动-反馈我习惯把智能体的工作过程拆成四个阶段感知把用户目标、历史上下文、工具返回结果组装成新的消息序列交给模型。决策模型根据指令和当前信息判断是直接给出最终回答还是需要调用某个工具。行动由你的代码调用真实的函数或外部接口系统因此产生实际效果。反馈把行动结果作为消息追加回去让模型看到“刚才那步到底成没成”。这四个阶段首尾相接就是一个循环。工程上所有花哨的概念——多智能体协作、带规划、带反思、带记忆——底层都是这个循环在转。你把这个循环想明白了后面读任何框架文档都会轻松得多。这里有个很容易忽略的点反馈这一步的质量直接决定整个智能体的上限。工具执行结果如果只是“ok”或者一串无格式的原始返回模型很难判断下一步该怎么走。我们后文会专门讲怎么设计工具返回值这是同样的坑我早期踩过不止一次。2. 智能体的四个核心零件模型、工具、记忆、编排2.1 大脑选型模型不是越大越好很多人的第一反应是“智能体肯定要用最强的大模型”这个想法在原型阶段没错但上了生产你就会发现模型的选择跟任务性质要匹配。判断标准我一般看三条一是工具调用的稳定性也就是模型能不能稳定输出格式正确的 tool_calls 指令二是上下文窗口够不够装下你的循环历史三是单次调用成本和延迟能不能接受。如果任务是“从一段客户工单里提取结构化字段然后调用一个查询接口”这种链路短、决策简单的场景一个中等规模的模型就够了。真正需要大模型的是长链路推理多步骤规划、需要从一堆互相矛盾的信息里做判断、或者工具数量非常多比如几十个需要模型从中精挑细选。有个很实用的经验先把模型写好之后留一个统一的模型适配层把模型名、服务地址、超时时间都做成配置。因为你会发现同一个智能体换个大模型跑行为差异可能非常夸张——有的模型遇到模糊指令就乱调工具有的模型打死都不调工具一定要实测对比后再定。2.2 手脚工具调用Function Calling是怎么工作的工具调用的底层机制说穿了并不神秘。模型还是正常生成文本但在生成最终回答之前它可以先输出一个特殊的 JSON 块里面声明“我想调用哪些函数、传什么参数”。你的代码收到这个声明后负责真正执行函数然后把结果作为一个新角色tool的消息放回对话里。打个比方模型是施工现场的指挥但它不搬砖你的代码是搬砖小队听指令干活干完还要汇报“砖搬完了还剩多少料”。指挥的职责是分活搬砖的职责是干活汇报的内容反过来又影响指挥下一步怎么安排。这个机制里最容易被低估的是工具描述description的写法。模型决定“该用哪个工具”完全靠工具名和描述来推断。描述里至少要说清楚三件事这个工具是干什么的、适合在什么场景下用、返回结果长什么样。比如“查询订单状态”和“查询订单明细”是两个很像的工具描述写得模糊模型就经常选错这是我见过最高频的生产事故之一。参数部分建议用 JSON Schema 明确定义类型和必填项。模型传参的类型错误虽然不多见但一旦出现你的执行端要有容错不能因为city传成了数字就直接崩溃。2.3 记忆短期上下文与长效记忆记忆是智能体最容易“先爽后痛”的部分。短期记忆其实就是消息数组本身每一轮感知都在往里面追加消息。问题在于工具调用循环跑起来后消息增长极快一个看似简单的任务可能一轮就产生十几条消息几轮下来上下文就爆了。我的处理思路是分两层。短期记忆用一个滑动窗口只保留最近 N 条消息更早的压缩成一段摘要放在最前面。摘要可以由模型生成也可以简单粗暴地只保留“用户原始目标 最后一步的工具结果”。很多场景下后一种够用还能省一次模型调用。长效记忆侧重保存跨会话的稳定信息比如用户的偏好、常用参数、之前验证过的结论。工程化做法是把这些信息写入向量库每次会话开始时按相关度检索几条塞进上下文。第一版我建议先别碰向量库用一个 SQLite 表按关键词查询就能顶住多数场景。记忆不是越复杂越好够用且可控才是第一位。2.4 编排主循环与控制策略编排层就是那个“循环”的代码实现核心是一句话在什么条件下继续调用工具在什么条件下停止。最基本的终止条件是模型不再输出 tool_calls直接给出最终回答但你必须额外加一重保险——最大循环步数。控制策略上有两个常见流派ReAct 模式和规划先行模式。ReAct 是边想边做每一步都根据上一次工具的结果来决策适合任务链路不确定、需要随机应变的场景。规划先行则是先用一次模型调用生成完整任务清单再逐个执行适合步骤清楚、可按部就班的任务。我的建议很简单第一版全部用 ReAct也就是不加额外的规划步骤让主循环自己跑。等数据积累起来发现模型经常在“先做什么后做什么”上犯糊涂再升级成规划先行。过早引入复杂编排只会让排查问题变得更困难。3. 手写一个最小可用智能体完整代码与逐段解析3.1 环境准备与依赖说明这里我不推荐一上来就上重型框架原因后面会讲。先准备一个干净的 Python 3.10 环境依赖只需要一个 HTTP 客户端requests 就够再加一个用于管理密钥的 dotenv总共不超过二十行安装命令。pip install requests python-dotenv你还需要一个兼容 chat/completions 协议的模型服务地址和密钥。现在主流的大模型服务商基本都兼容这套协议你在代码里配置好接口地址、密钥和模型名即可。配置文件我习惯用.env避免密钥进代码库。为什么要坚持“不用框架先手写”因为框架帮你隐藏了太多细节一旦出问题你根本不知道是模型的错、工具的错还是框架编排的错。手写一遍主循环把所有细节暴露在眼前之后你用框架就是“带着答案看代码”而不是“带着疑问调黑盒”。3.2 定义工具JSON Schema 是给模型看的说明书先定义两个演示工具一个查询天气一个做四则运算。工具定义分两部分给模型看的 Schema和给代码执行的函数。import json import requests LLM_ENDPOINT https://your-api-endpoint.example/v1/chat/completions API_KEY YOUR_API_KEY MODEL your-model-id TOOLS [ { 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: { expr: {type: string, description: 数学表达式如3 * (4 5)} }, required: [expr] } } } ] def get_weather(city: str) - str: # 生产环境应替换为真实天气服务接口 return f{city} 当前 24℃晴微风 def calculate(expr: str) - str: # 注意这里只是演示生产环境绝对不要直接 eval 用户输入 # 建议改用 ast.literal_eval 或专门的计算表达式库 try: return str(eval(expr, {__builtins__: None}, {})) except Exception as e: return f计算失败{e} TOOL_MAP { get_weather: get_weather, calculate: calculate, }注意工具描述里我把“什么时候用”写得很明确还顺带写了“什么时候不要用”比如计算工具只支持四则运算。这种边界描述能显著降低模型选错工具的概率。另外我在计算函数里显式捕获了异常并返回可读的错误信息——这个设计后面会解释为什么重要。3.3 主循环代码一次完整的“想-做-看”过程接下来是核心的主循环。先写一个调用模型的辅助函数再写智能体的运行函数。def call_llm(messages, toolsNone): payload { model: MODEL, messages: messages, tools: tools or [], temperature: 0.2, } resp requests.post( LLM_ENDPOINT, headers{Authorization: fBearer {API_KEY}}, jsonpayload, timeout60, ) resp.raise_for_status() return resp.json()[choices][0][message] def run_agent(user_query: str, max_steps: int 5) - str: messages [{role: user, content: user_query}] for step in range(max_steps): msg call_llm(messages, toolsTOOLS) messages.append(msg) if msg.get(tool_calls): for tc in msg[tool_calls]: fn_name tc[function][name] args json.loads(tc[function][arguments]) result TOOL_MAP[fn_name](**args) messages.append({ role: tool, tool_call_id: tc[id], content: json.dumps(result, ensure_asciiFalse), }) else: return msg[content] return 达到最大步数限制任务提前结束这段代码不到三十行但已经把智能体最小的骨架完整实现了。几个关键点值得细说temperature0.2工具调用阶段需要行为稳定温度太高模型会“发挥”可能多调工具或少调工具0.2 是我实测比较稳的起点。tool_call_id必须原样传回模型靠这个 ID 把工具结果和之前的调用对应起来漏了或写错服务端直接报错。工具名在执行前最好校验是否在TOOL_MAP里防止模型“发明”出根本不存在的工具名。工具结果统一转成 JSON 字符串返回保证模型能稳定解析。测试一下效果print(run_agent(上海今天天气怎么样)) # 期望输出上海 当前 24℃晴微风 print(run_agent(帮我算一下 3 * (4 5) 等于多少)) # 期望输出27第二个提问会触发完整循环模型先决定调用 calculate代码执行后把结果喂回模型再基于结果输出最终答案。整个链路一跑通最小智能体的“骨骼”就立起来了。3.4 加入记忆管理后的完整升级版最小版有个明显隐患消息数组只增不减。跑几个来回没问题任务一多就爆。我加一个非常简单的滑动窗口逻辑保留最近 6 条消息并用一个系统消息记住用户的原始目标def trim_messages(messages, max_len20, system_noteNone): if len(messages) max_len: return messages head messages[:1] tail messages[-(max_len - 1):] if system_note: head [{role: system, content: system_note}] return head tail这个函数做两件事保住第一条用户消息或者换成系统级目标说明然后截断中间历史。只要任务的当前状态能从“目标 最近几步工具结果”里还原这个裁剪策略就足够用了。等你做到更复杂的任务再考虑让模型对旧历史做摘要压缩那个开销更高但信息损失更小。4. 实操中踩过的坑与排查技巧手写一遍主循环只是万里长征第一步。真正让你怀疑人生的永远是那些“看起来一切正常但结果就是不对”的边界情况。下面几条都是我在真实项目里踩过的按出现频率排序。4.1 工具返回格式不规范导致模型“看不懂”第一次写智能体时我的工具函数直接返回 Python 字典落库的时候忘了转成字符串。结果模型收到的 tool 消息是一坨 repr 风格的文本引用全是单引号字段顺序还随机。模型勉强能读但经常在下一步把字段名理解错导致后续判断全乱。规范化做法是工具返回一律统一为可解析的 JSON 字符串字段名固定保持稳定。还有个细节错误要返回给模型不要吞掉。如果工具执行失败把“失败原因”原样返回模型才知道换个参数重试或者换条路径你吞掉异常返回一个空字符串模型会以为查询正常但没结果然后开始编造。4.2 上下文膨胀与费用失控智能体的每一条消息都计费而且工具调用循环天然就会产生大量中间消息。我见过一个原型任务用户只问了一句“帮我分析这份报表”跑完用了 40 多条消息其中 30 条是工具往返Token 消耗是普通对话的十倍还多。这里有两个控制手段。一个是程序层面的max_steps这个必须有我一般从 5 开始调根据任务复杂度放宽但再宽也很少超过 15。另一个是上下文裁剪策略就是 3.4 节写的滑动窗口。除此之外最好给每次运行记录一下 Token 消耗跑完打印出来你就会对“智能体烧钱速度”有非常直观的敬畏。4.3 死循环与重复调用有一次模型反复调用同一个查询工具参数一字不差连续循环了七次才被 max_steps 拦下。排查后发现是第一次调用返回的结果里有几个特殊字符模型误以为查询失败于是用同样参数发起重试每一步还把“上次我也查过”这件事给忘了。解决思路是加一个“重复调用检测”记录最近几次的工具名和参数如果连续出现两次完全相同的调用直接中断并将“该结果已在上一步获得请基于它回答”作为反馈返回给模型。这个方法简单但非常有效强烈建议加上。另外要在请求层设超时。requests 的 timeout 参数要设整个循环还要设一个总运行时间上限。智能体失控时往往是“静默烧钱”跑得越久你亏得越多超时机制是最后的止损线。4.4 模型选错工具或虚构结果选错工具多半是描述写得不够清楚。我把两个相似工具的描述重写之后误选率大幅下降。一个技巧在 description 里显式写明“当……时使用当……时不要使用”。模型读描述就是读说明书说明书边界越清晰行为越可控。虚构结果则是更麻烦的问题。当工具真的没有返回数据时模型很容易顺着用户的预期编一个“看起来合理”的答案。应对策略分两层第一层工具返回里不能只有空数组要加一句明确的状态说明比如“未查询到该订单的任何记录”第二层在系统提示里写死一条规则“当工具返回为空或明确报告失败时你必须如实告知用户不得推测或编造。”把这条规则放在靠近系统提示开头的位置压制效果最明显。4.5 常见问题速查表现象根因排查思路模型反复调用同一工具上一步结果被忽略或误判为失败检查工具返回文本编码增加重复调用检测工具结果解析报错函数返回了非 JSON 内容统一用 json.dumps 序列化异常全部捕获为字符串上下文超限循环消息无节制增长加滑动窗口裁剪加 max_steps 上限模型“发明”不存在的工具名工具列表传入遗漏或模型幻觉执行前校验 TOOL_MAP确认每次请求都带全 tools 参数用户问题简单但模型乱调工具工具描述边界不清重写 description加“不要使用”场景结果正确但回答风格离谱温度过高或缺少风格提示工具阶段温度降到 0.2 以下系统提示补充回答格式要求5. 让智能体从“能跑”到“可靠”的经验法则5.1 用结构化输出约束模型工具调用之外的很多环节也需要模型输出结构化内容比如“你判断这个任务是否需要人工介入”如果让模型自由发挥你得到的可能是十种不同说法的自然语言。解决方案是让模型按固定格式返回。现在主流服务商基本都支持响应格式约束指定response_format为 JSON 对象即可。我一般在需要模型做判断、打分、提取字段的时候开启这个开关同时会在提示里给出明确的输出字段定义。开箱感受模型输出从“散文”变成“工单”代码解析成本直线下降。5.2 最小权限工具越窄越安全给智能体的每个工具都问一个问题这个工具真的需要暴露全部能力吗如果查询接口能传任意 SQL那模型一旦被提示词注入诱导就可能执行危险操作。我的原则是能只读不写、能限定参数范围就限定涉及写操作和敏感数据的工具一律走“人机回环”——模型产出建议真正执行前由人来确认。这就像给实习生权限你只给他单张报表的查看权限而不是全公司数据库的管理员权限。权限收得越窄出大事的窗口越小。智能体的工具权限设计本质上和给内部系统设计 API 权限是同一套思路。5.3 可观测性日志里至少要能看到什么智能体最大的排查难点在于过程不可见。你只知道最终答案不对但不知道是哪一步开始跑偏的。我给生产环境的智能体定了最低日志要求每轮记录四个维度的信息——输入时的消息长度、模型返回的决策内容、工具名称与参数、工具返回结果的摘要。四个维度拼起来就能完整还原智能体的“思维轨迹”。日志格式我习惯用 JSON 行每行一条事件方便后续接日志平台和统计分析。跑完一次任务除了结果一定要能看到 Token 消耗和步数这两个指标直接反映智能体的“健康程度”。一个正常任务如果经常用满 max_steps说明编排逻辑有问题不是模型不够聪明就是工具设计不合理。5.4 给智能体建一套回归测试智能体开发有个很讨厌的特点模型是非确定性的同一段代码今天跑过明天可能表现不一样。所以靠“手动点两下觉得没问题”来判断完全不可靠。我建议从第一天就建一个固定测试集哪怕只有二十个用例。每个用例至少断言两件事是否调用了期望的工具通过日志里的 tool_calls 判断最终回答是否包含关键信息。跑回归时把温度固定尽量隔离随机性。改提示词、换工具描述、升级模型之后都跑一遍这个测试集你会发现很多“感觉没问题”的改动其实悄悄破坏了一部分行为。没有测试集保护的智能体迭代纯粹是在靠运气往前走。6. 接下来可以往哪走6.1 多智能体协作主循环稳定之后自然会想拆多个角色一个规划师负责拆解目标一个研究员负责查资料一个执行者负责调操作型工具一个审查者负责把关结果。多智能体不是把几个单智能体简单拼起来核心难点在通信协议角色之间传什么消息、谁来终结对话、结果以谁为准。我的建议是先从两个角色试起把协议跑通再逐步加角色一次只加一个。6.2 记忆与反思机制把对话历史、工具执行结果、甚至失败教训都沉淀下来会让智能体在后续会话里越来越“懂行”。反思机制的做法是一次任务失败或结果不佳后让模型回顾整个过程写一段“哪里出了问题、下次怎么改”存成经验条目。下一次类似任务来临时先把相关经验检索出来放进上下文。这个机制的工程成本不低但带来的体验提升非常明显。6.3 评估驱动迭代最后一个建议也是我反复给团队强调的没有评估就没有迭代。先把二三十个典型任务沉淀成基准集每次改动都跑一遍把“正确率”“平均步数”“平均 Token 消耗”三个数记下来。你会发现很多你以为的优化其实只是在小样本上碰巧有效。有了评估基线你的智能体才能确定性地一步步变好而不是偶尔灵光乍现。我在实际项目里带过好几个智能体从原型走到生产最深的体会是智能体的上限由模型决定下限由代码决定。模型再强没有可靠的工具定义、没有严格的循环约束、没有清晰的记忆管理照样会给你跑出一堆匪夷所思的结果。反过来只要把主循环、工具规范、控制策略这三件基本功做扎实哪怕用中等规模的模型也能稳定完成大量真实任务。第一篇到这里就把最小可运行的结构讲完了。下一篇我打算具体聊聊工具生态怎么设计——什么时候该把能力拆成细粒度工具什么时候该合并成一个大工具以及工具之间如何编排才不会互相打架。如果你正在写自己的第一个智能体建议先把文中的主循环代码跑起来改一改工具加一加日志体会一遍从“模型说话”到“代码干活”的完整链路再回来读下一篇收获会大很多。