AI Agent单次模型请求:标准对话、工具调用与流式的工程选型

发布时间:2026/10/14 5:21:26
AI Agent单次模型请求:标准对话、工具调用与流式的工程选型 做 AI Agent 的开发者大概率都经历过这个阶段一开始觉得模型接口就是“发一段文本收一段文本”这么简单直到你发现同一个 Agent 在不同场景下对“单次模型请求”的诉求完全不一样。你让 Agent 查天气模型可能编一个假天气给你你想让返回结果直接落成结构化数据模型偏偏在文本里夹一句“好的这是您要的数据”你嫌回复太慢把流式打开之后工具调用的结果又变成了一堆支离破碎的增量片段。这些问题本质上是单次模型请求的方式没选对也可能是三种方式混着用的时候没理清边界。我理解的“单次模型请求”指的是 Agent 在一个决策循环里向大模型发出的单次调用——不是整个 Agent 的完整对话流程而是流程里最小的一次“输入 → 输出”单元。这一单元在工程上主要有三种主流请求方式标准对话请求、工具调用请求、流式请求。这篇文章不打算对着接口文档照本宣科而是把我在实际项目里对这三种方式的拆解、踩坑和选型经验梳理出来给正在做 Agent 或者准备上手 Agent 的同学一份可以直接参考的实践笔记。1. 三种请求方式的分工逻辑先搞清楚它们在解决什么问题1.1 Agent 里的“单次模型请求”到底指什么在讨论代码之前有必要先把概念拧清楚。很多教程一上来就甩 API 参数但你问一句“这三种请求到底有什么不同”对方可能说不上来。实际上Agent 的完整运行流程是一个“感知 → 决策 → 行动 → 再感知”的循环这个循环每转一圈模型往往要被调用好几次第一次判断用户意图第二次决定要不要调用工具第三次把工具结果整理成自然语言。每一次调用都是独立的输入输出交换这就是“单次模型请求”。为什么要把“单次”单独拎出来讲因为排查 Agent 问题的时候你面对的往往不是整个流程而是某一次请求。比如用户反馈“偶尔回答很离谱”你无法直接复现整个循环但你能检查其中每一次请求messages 组装是否正确、tools 定义是否合理、流式解析是否丢帧。把单次请求当成可观测、可测试的单元Agent 才不是一个黑盒。更关键的是单次请求的三种方式在 Agent 里承担的是不同职能互相不能替代。核心对话负责“想”工具调用负责“做”流式传输负责“说”。“想”得清楚不代表“做”得对“做”得对也不代表用户等得不焦虑——三者是 Agent 运转的三个齿轮而不是三个可以随便替换的候选方案。1.2 一张表看懂三种方式的分工与差异我习惯用一张表把三者放在一起对比给团队做分享时也从这张表讲起请求方式核心解决的问题返回形态在 Agent 中的角色标准对话请求模型如何生成文本答案完整 JSON答案在 content 字段决策中枢的基础调用工具调用请求模型如何判断并调用外部工具完整 JSON行动方案在 tool_calls 字段行动规划器流式请求生成过程如何不让用户干等SSE 增量 chunk用户交互通道可以看出三种方式针对的是不同层面的问题。标准对话请求关心“模型说什么”工具调用请求关心“模型做什么”流式请求关心“用户怎么听”。一个功能完整的 Agent通常会把三者组合起来用内部决策用标准请求需要外部数据时切换工具调用面向用户的输出开流式。所以接下来的三章我会把每种方式单独拆开讲清楚——从请求结构到响应处理再到我实际踩过的坑。2. 方式一标准对话请求Agent 的基础通信单元2.1 请求构造的最小完整示例标准对话请求是最容易上手的方式也最容易被用错。先看一段基于原生 HTTP 的最小示例。我习惯先用裸 HTTP 做底层封装因为不依赖任何 SDK能看清协议本身的逻辑import requests resp requests.post( https://your-api-endpoint/v1/chat/completions, headers{ Authorization: Bearer YOUR_API_KEY, Content-Type: application/json, }, json{ model: your-model-id, messages: [ {role: system, content: 你是 Agent 的核心决策模型回答要简洁不要编造数据。}, {role: user, content: 今天北京适合穿什么衣服}, ], temperature: 0.3, max_tokens: 500, }, timeout30, ) data resp.json() answer data[choices][0][message][content] print(answer)这段代码里有几个容易被忽略的细节。第一是messages的结构。很多人以为 system 只是“角色扮演”但在 Agent 场景里system 承担的是行为约束比如“必须通过工具获取实时信息”“不要编造数据”。我在项目里就吃过亏system 只写了“你是一个助手”模型在回答实时问题时自信地编了一大段后来把约束明确写进 system幻觉率才明显降下来。第二是temperature。Agent 内部决策链路上我通常设置 0.2 到 0.4太高会让模型在需要确定性判断时过度发散只有面向创意类任务才会调高。第三是timeout。单次请求如果超过 30 秒还没返回用户侧的体感已经非常差了。如果模型经常超时应该考虑换成流式请求而不是把超时时间继续加大。2.2 响应结构、用量统计与上下文管理标准对话请求的响应包含几个关键字段。choices[0].message.content是模型生成的文本这是最常用的但message.role同样重要——它是assistant构造下一轮请求时你必须把这条 assistant 消息追加回 messages 数组否则多轮对话的上下文就断了。我见过不少同学在循环里只堆 user 消息把模型上一轮回复丢掉结果模型聊着聊着就“失忆”究其原因就是上下文不完整。另一个实用字段是usage记录 prompt tokens、completion tokens 和 total tokens。对 Agent 来说它不只是计费用更是一个性能观测指标。如果每次请求 prompt tokens 都在涨说明上下文管理出了问题。我现在的做法是在组装 messages 前先估算 token 数超过阈值就做两件事把最早的历史消息截断或者用模型对早期对话做一次摘要压缩后再嵌入上下文。2.3 标准对话请求在 Agent 里的真正位置标准对话请求适合三类 Agent 场景。第一类是不需要外部工具的问答比如文档助手模型自身知识足够回答大部分问题第二类是工具调用循环里的收尾请求工具结果回来后用一次标准请求把结构化数据整理成用户能看懂的答案第三类是单步的意图识别、分类、抽取这类任务通常要求结果稳定不需要流式。但如果你让 Agent 执行“查天气”“查订单”“检索内部知识库”这类需要外部动作的任务标准对话请求就力不从心了。模型没有渠道接触实时数据只能基于训练时的记忆来“编”这就是幻觉的来源。所以标准对话请求是 Agent 的地基但光靠地基盖不起真正的智能体——工具调用请求才是 Agent 与普通聊天机器人拉开差距的分水岭。3. 方式二工具调用请求让模型在单次请求里交出“行动方案”3.1 工具调用的原理先写解题计划工具调用请求Function Calling / Tool Calling依然是单次模型请求但在请求体里多了一个tools数组。模型看到工具定义后会判断当前任务是否需要调用某个工具如果需要它并不直接生成最终回答而是在返回的tool_calls字段里给出“函数名 参数”。一个很形象的类比标准对话请求是让模型直接答题工具调用请求是让模型先写解题计划。Agent 拿到计划后自己去执行工具再把工具的真实结果喂回模型模型基于真实结果完成最终回答。整个“计划 → 执行 → 反馈”的过程里每一步都是一次独立的模型请求但“要不要用工具”这个决策就发生在工具调用请求的那一次。理解这一点你就能明白为什么 Agent 的惯例是“先调一次模型判断行动方案执行完工具再调一次模型生成答案”而不是让模型直接输出。3.2 工具定义 JSON Schema 的实操写法工具定义本质上是 JSON Schema。下面是我在一个天气查询 Agent 里用过的定义结构上很有代表性{ type: function, function: { name: get_weather, description: 获取指定城市当天的天气情况包含温度、湿度和降水概率, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, date: { type: string, description: 日期格式为 YYYY-MM-DD缺省为今天 } }, required: [city] } } }写工具定义有三个值得留意的经验。第一description要写清这个工具解决什么问题、什么情况下使用。模型是靠 description 做工具选择的描述越精确误调用越少。第二参数 description 里加示例值比如 city 写“北京、上海、广州”模型更容易生成合法参数。第三required要克制只放真正必要的参数。必填参数越多模型生成非法参数的概率越高工具执行层就要花更多精力做校验。3.3 处理 tool_calls 返回与二次请求模型返回tool_calls后Agent 的标准处理流程如下data resp.json() message data[choices][0][message] # 判断模型是否要求调用工具 if message.get(tool_calls): for call in message[tool_calls]: fn_name call[function][name] fn_args json.loads(call[function][arguments]) result execute_tool(fn_name, fn_args) messages.append({ role: assistant, content: None, tool_calls: [call], }) messages.append({ role: tool, tool_call_id: call[id], content: json.dumps(result, ensure_asciiFalse), }) # 发起第二次模型请求模型基于真实工具结果输出最终答案 resp2 requests.post( https://your-api-endpoint/v1/chat/completions, headersHEADERS, json{model: YOUR_MODEL, messages: messages}, timeout30, )这个流程里有一个必须强调的细节tool_calls不是普通文本每一条都有自己的id回传工具结果时必须带上tool_call_id指向对应调用而且要按顺序把 assistant 的tool_calls消息原样追加回去。很多初学者把工具结果塞进 user 消息模型要么报错要么就“看不到”这次工具执行。另外一次请求里模型可能返回多个tool_calls也就是并行调用多个工具这时候要逐个执行、逐个回传顺序不能乱。3.4 工具请求上我踩过的三个坑第一个坑是JSON Schema 定义过宽导致模型硬凑参数。我之前给信息检索工具定义了filters这种自由格式对象模型经常生成格式合法但含义完全不对的过滤条件工具执行出来全是空结果。后来我把 filters 拆成明确的字段枚举误调用率立刻降下来了。对 Agent 来说“工具好调用”往往比“工具功能强大”更重要因为模型不是人它不会像你一样理解隐式的参数约定。第二个坑是工具结果超长。模型返回的 tool 消息会完整进入下一轮请求的上下文如果工具结果是一次数据库全表扫描上下文几万 token 就没了。现在我在工具执行层统一做裁剪默认只保留前 2000 个字符并附上“结果已被截断完整数据共 X 条”的提示让模型知道数据没被塞进来需要时可以换更精确的查询条件。第三个坑是**tool_choice的误用**。默认auto是让模型自己判断是否调用工具required强制模型必须调用传具体工具名则强制调用指定工具。我曾在一个人工审核场景里为了省一次请求直接强制指定工具结果用户消息明显不需要工具时模型也强行调用流程直接卡死。强制模式只在你对编排逻辑有十足把握时再用否则宁可多花一次请求换稳定性。4. 方式三流式请求把首字延迟打下来的关键4.1 流式返回的底层逻辑边生成边推送第三种方式是流式请求。它的原理是请求时带上stream: true模型不再等完整回复生成完毕而是通过 SSEServer-Sent Events边生成边推送每个 chunk 是一段文本增量。为什么 Agent 要做流式因为大模型生成几百个 token 可能耗时十几秒用户在这十几秒里盯着空白页面体验非常糟糕。流式请求让第一个 token 在几百毫秒内到达后续内容持续刷出来用户会觉得“系统在正常工作”。对 Agent 来说流式还有一个额外收益工具调用循环往往比普通问答更耗时如果不流式用户看到的是一段更长时间的沉默很容易误以为服务挂了。4.2 用原生 HTTP 实现流式请求下面是我用原生 Python 实现 SSE 解析的写法不依赖 SDK方便理解底层逻辑import json import requests resp requests.post( https://your-api-endpoint/v1/chat/completions, headersHEADERS, json{model: YOUR_MODEL, messages: messages, stream: True}, streamTrue, timeout(10, 60), ) content_parts [] for line in resp.iter_lines(decode_unicodeTrue): if not line.startswith(data:): continue data line[5:].strip() if data [DONE]: break chunk json.loads(data) delta chunk[choices][0][delta] if content in delta and delta[content]: content_parts.append(delta[content]) print(delta[content], end, flushTrue) full_text .join(content_parts)这里有两个细节要特别注意。第一timeout要传元组(连接超时, 读取超时)。连接可能很快建立但后续每个 chunk 的间隔可能很长如果只设一个总超时长回复会在中途被掐断。第二SSE 每行以data:开头空行是事件分隔符[DONE]是结束标记这些都需要自己解析。SDK 把这些封装好了但线上排查时理解原始协议才能快速定位问题。4.3 流式与工具调用的组合问题流式请求和工具调用组合时情况会变得微妙。模型输出可能是文本和tool_calls混合的而且tool_calls在流式模式下会分片到达——一个完整的 arguments JSON 被拆成多个增量片段每个片段带着index字段标识属于第几个工具调用。你需要按 index 聚合。前面 4.2 节里的解析逻辑可以抽成一个辅助函数这里简写为parse_sse(line)重点看聚合部分tool_call_chunks {} for line in resp.iter_lines(decode_unicodeTrue): data parse_sse(line) if not data: continue delta data[choices][0][delta] if tool_calls in delta: for tc in delta[tool_calls]: idx tc.get(index, 0) if idx not in tool_call_chunks: tool_call_chunks[idx] {name: , arguments: } if tc.get(function, {}).get(name): tool_call_chunks[idx][name] tc[function][name] if tc.get(function, {}).get(arguments): tool_call_chunks[idx][arguments] tc[function][arguments]聚合完成后再按 index 顺序把 tool_calls 组装进 messages走正常的工具执行流程。这段逻辑手写很容易出错尤其是多个工具并行调用时index 顺序一乱arguments 的 JSON 解析就会失败。如果项目对稳定性要求高我建议直接用官方 SDK 处理流式加工具调用把分片聚合交给底层库自己只关注业务逻辑。4.4 流式请求最容易忽视的细节流式请求最容易翻车的三个细节我一个个说。第一个是连接被闲置断开。某些模型服务在长时间无数据时会关闭连接而模型的思考阶段可能长达几十秒。解决思路是设置合理的读取超时并支持重连。另外如果流式请求后面还跟着工具调用那么工具执行期间流已经关闭前端界面要处理好“首段流结束 → 工具执行中 → 次段流开始”的状态衔接不然用户体验是断层的。第二个是消费端缓冲。我见过有同学在 Web 服务里把流式响应完整收完再一次性返回给前端首字延迟直接被打回原形。流式的意义在于端到端后端收到 chunk 就应立即向下游转发任何中间层的整体聚合都会破坏这个效果。第三个是用量统计。有些服务商支持stream_options: {include_usage: True}这样最后一个 chunk 会附带 usage 统计。流式模式下如果你还需要统计 token 消耗记得开启这个选项否则只能靠估算。5. 三种方式的组合实战与选型建议5.1 判断标准先看场景再看传输三种方式不是互斥的应按场景组合。我给团队定的判断标准先看场景特征再看底层传输场景特征推荐方式原因内部决策、意图识别、文本分类标准对话请求不涉及用户直接交互追求结果稳定需要调用外部工具或查询实时数据工具调用请求Agent 的核心交互模式面向用户的文本生成、对话回复流式请求首字延迟低交互体验好工具调用加用户可见回复流式请求搭配工具调用先流式反馈再工具调用再流式给结果超长文本生成流式请求避免长时间无反馈误以为服务中断这张表是我在项目里反复调整后沉淀下来的。你可以看到没有一种方式是“万能答案”。我见过有人把所有请求都开流式内部意图识别也跟着流式结果反而增加了解析开销也有人把用户可见回复做成同步请求被投诉“转圈太久”。选型的关键是先问一句这一次请求的用户是谁是外部真实用户还是 Agent 内部的逻辑步骤用户可见的输出开流式内部逻辑保持同步和确定性这是我目前最顺手的默认策略。5.2 一个真实 Agent 任务的混合请求策略拿一个客服工单助手举例。用户说“帮我查一下上个月的快递单为什么还没到”。整个流程实际是这样走的。第一步Agent 发起一次标准对话请求做意图识别判断这是查询类任务而不是闲聊第二步Agent 发起一次工具调用请求模型返回“调用 get_orders 工具参数 user_idxxx”第三步Agent 真实执行订单查询接口拿到工单状态第四步Agent 再发起一次流式请求把查询结果生成自然语言并流式推送给用户。用户在界面里看到的是一段在几秒内逐渐刷出来的完整回答流畅自然。但底层其实是四次独立的单次模型请求标准对话、工具调用、流式全都用上了。这种混合策略的核心思想是对用户透明的内部步骤用标准请求需要外部数据的决策用工具调用用户直接面对的输出用流式。三层各司其职Agent 才能既智能又不拖沓地完成工作。5.3 供应商差异与统一封装的建议最后聊一点跨厂商的经验。不同模型供应商的 API 在命名和字段上各有差异但单次请求的概念框架高度一致都有 messages 或类似结构都支持 tools 或 functions 声明都支持流式返回。我在项目里会在底层封装一个统一的请求层把标准对话、工具调用、流式抽象成三个方法。切换供应商时只需要替换端点、鉴权和消息字段映射业务层代码基本不动。这里有个反面教材值得说。我最初为了省事直接在某框架之上写业务结果线上反馈“工具调用偶尔失败”时我只能重启服务、加日志完全插不进手排查——因为框架把 tool_calls 的组装逻辑都封装起来了。后来我抽出时间把三种请求方式都用裸 HTTP 亲手实现了一遍再回头看框架很多曾经看不懂的配置项忽然就通了。我个人现在的建议是不要一上来就引入重量级 Agent 框架。先用裸 HTTP 或官方 SDK 把标准对话、工具调用、流式这三种单次模型请求吃透。框架可以帮你提速但不能帮你理解问题。三种请求方式都亲手写过之后框架对你来说就只是一个可选加速器——用不用、怎么用主动权都在自己手里。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询