大模型Agent请求模型的三种方式:文本补全、函数调用与结构化输出

发布时间:2026/10/20 21:41:44
大模型Agent请求模型的三种方式:文本补全、函数调用与结构化输出
最近帮两个团队做Agent落地代码评审发现一个特别有意思的现象大家聊架构、聊记忆、聊多智能体协作都头头是道但一问到单次模型请求具体是怎么发的十个人里有七八个会愣一下。有的直接把所有历史消息一股脑塞进上下文有的让模型返回一大段自然语言再自己写正则去抠关键字段还有的干脆让Agent循环调用模型直到输出“看起来对”为止。这个不起眼的环节恰恰是Agent系统里最要命的地方。模型请求怎么构造、要什么输出格式、怎么和外部工具对接直接决定了整个Agent的稳定性、可控性和成本。我在实际项目中试过各种调用组合踩了不少坑之后总结下来请求单次模型这件事主流做法其实就是三种纯文本补全、函数调用、结构化输出。这篇文章就把三种方式的原理、代码、选型思路和坑位一次讲透。1. 为什么单次模型请求是Agent的核心动作1.1 先重新理解Agent的执行循环很多人对Agent的理解是“自动调用模型干活”这个认知没错但太粗了。真正跑过Agent的人都知道Agent的本质是一个循环拿到用户目标拆解成步骤每一步可能调用模型做判断或生成内容然后根据结果决定下一步动作直到任务完成。在这个循环里单次模型请求就是每个循环节拍上的“那个瞬间”。它决定了Agent当前这一步能获得什么信息、做出什么决策。打个比方Agent像是一个流水线工人每处理一个工件都要抬头看一眼图纸单次模型请求就是这个“抬头看一眼”的动作。图纸给错了、看的方式不对后面整条流水线都是错的。所以请求模型不是简单地把问题丢给大模型就完事。你需要明确三个问题上下文里该带什么期望模型输出什么形态的结果这个结果如何被后续代码可靠地消费。1.2 三种方式本质上是三种输出契约为什么市面上会有这么多种调用方式核心原因是大模型的输出天然是自然语言而程序需要的是结构化、可解析的数据。自然语言是给人类看的程序拿到一段散文根本没法直接处理。纯文本补全是最原始的方式模型返回一段文本内容自由解析靠正则或运气。函数调用则让模型在输出中附带一个结构化的“工具调用请求”明确表达“我想调用某某工具参数是某某”。结构化输出更进一步强制模型的输出符合预设的JSON Schema。三种方式没有绝对的好坏只有适不适合。它们代表了你和模型之间三种不同的“契约”第一种契约是“你说你的我自己听”第二种是“请明确告诉我要调哪个工具”第三种是“请按我的格式给我数据”。理解了这个本质选型就不会纠结了。2. 方式一纯文本补全最原始但依然常用2.1 它解决什么问题纯文本补全也就是直接调用模型的文本生成能力把对话历史或提示词发过去模型返回一段自然语言。这是最基础的调用方式也是很多人接触大模型时学会的第一种方式。它并不像有些人想的那样“过时了”。在Agent场景里很多环节其实只需要模型生成文本不需要什么结构化约束。比如让Agent对一段长文档做摘要、给用户生成一段友好的回复、把一堆散乱的需求改写成规范的行动列表、对用户情绪做一个倾向判断——这些场景的输出天然是文本硬要套JSON反而别扭。我见过一个做会议纪要Agent的项目最开始团队给所有模型输出都套了JSON Schema连“生成一句总结”这种输出都要包一层。结果模型经常为了凑JSON结构把原本自然的句子改得生硬无比。后来改成纯文本补全反而质量直线上升。这说明一个道理输出契约越严格模型的自由表达空间越小人类可读内容的自然度就越低。2.2 最小可用代码长什么样用最常见的方式展示构造消息并请求模型from openai import OpenAI client OpenAI(api_keyyour-api-key) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个简洁的摘要助手只用3句话概括用户输入的核心信息。}, {role: user, content: 我们想做一个新的智能客服系统希望能在用户咨询时自动判断问题分类并给出初步解决方案。现有系统响应时间长人工成本高。} ], temperature0.3, max_tokens200 ) result response.choices[0].message.content print(result)这段代码里关键在于系统提示词的设计。你需要在system里明确输出长度、语气风格、内容边界。很多新手把system当摆设随便写一句“你是一个有用的助手”那模型返回的内容质量就完全靠运气。2.3 参数调节与解析痛点实际调参的时候最重要的几个参数要弄清楚temperature控制随机性。做摘要、分类、抽取这类确定性任务建议调到0到0.3之间做创意生成、头脑风暴可以调到0.7以上。max_tokens限制输出长度。注意这个值包含思考过程的token不是纯输出的上限。top_p配合temperature使用一般场景调一个就好不建议两个一起大幅调整行为会互相干扰。纯文本补全最大的坑在于后续解析。模型可能在一段话里混入额外说明比如你让它只输出分类结果它偏偏来一句“根据您的描述我认为该问题属于售后类别”。这时候你的代码需要写一堆清理逻辑去剥掉前缀、后缀、多余标点。这种脆弱解析在Demo阶段勉强能用一旦进入到生产环境用户输入千奇百怪模型输出也跟着飘解析规则会越写越长最后变成一个没人敢动的垃圾堆。所以我的建议是纯文本补全适合输出给人类看的内容不适合输出给程序解析的数据。如果下一步代码要依赖这个结果做分支判断请用下面两种方式。3. 方式二函数调用Agent连接外部世界的标准姿势3.1 函数调用的真正含义函数调用在业界通常叫Function Calling或Tool Calling。很多人第一次听到这个名字时有个误解以为是模型直接执行了代码。其实不是。模型的本职仍然是“生成文本”只不过当我们把工具的定义以特定格式传给模型后模型可以在回复里附带一个结构化的JSON对象表示“我需要调用某某函数参数值是某某”。真正去执行这个函数的是你自己的代码。这就像是模型是一个接线员它收到用户的需求后写了一张派工单请仓库管理员把订单A的商品数量改为10。程序拿到这张派工单再自己去找仓库管理员执行。这个设计精妙的地方在于它把“语义理解”和“动作执行”彻底解耦了。模型负责理解意图、提取参数系统负责执行动作。参数提取错了、工具选错了都有迹可循。3.2 一个完整的工具调用示例以查询订单状态为例先定义一个查询函数和它的描述然后把它作为tools传给模型from openai import OpenAI client OpenAI(api_keyyour-api-key) tools [ { type: function, function: { name: query_order_status, description: 根据订单号查询订单当前状态, parameters: { type: object, properties: { order_id: { type: string, description: 用户的订单号例如 ORD20240001 } }, required: [order_id] } } } ] messages [ {role: system, content: 你是电商客服助手根据用户的订单号查询订单状态。}, {role: user, content: 帮我看看订单ORD20240001现在到哪了} ] response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, tool_choiceauto ) message response.choices[0].message print(模型返回的完整消息, message)如果模型判断需要调用工具返回的message里会包含tool_calls字段里面是结构化数据if message.tool_calls: tool_call message.tool_calls[0] print(要调用的函数, tool_call.function.name) print(函数参数, tool_call.function.arguments)然后你执行本地函数把结果追加到消息历史里再发给模型生成面向用户的回复。这一步不能省否则模型不知道工具执行的结果只能凭空编。3.3 tool_choice和描述设计的经验tools定义好之后还有两个细节直接决定成败。第一个是tool_choice参数。默认auto表示让模型自己判断是否调工具可以设为none强制不调也可以指定某个具体函数名强制模型只能选它。对于那种“这轮必须查数据库才能回答”的场景设置tool_choice为指定函数能减少模型犹豫不决的情况响应更快也更稳定。第二个是函数描述和参数描述的措辞。很多人不重视description随便写一句就完事。实际上模型理解函数用途几乎完全靠description。写得模糊模型就会在你有一堆工具时选错写得过于啰嗦又干扰模型提取参数。我的经验是description用一两句话说明函数干什么、什么情况下调用参数的description说明格式和示例。例如order_id的说明写成“用户的订单号例如ORD20240001”比单纯写“订单号”要好得多因为模型看到了具体格式提取错误率会显著下降。还有一个容易被忽略的坑工具数量不要太多。模型在十几个工具里做选择时准确率会明显下降。如果一个Agent需要管理二十个工具建议做一个“工具调度器”先让模型从粗粒度分类里选再进入具体工具细分。这个分层设计能有效降低选择错误率。4. 方式三结构化输出让模型直接给你干净的JSON4.1 结构化输出解决的问题第三种方式是结构化输出业界也叫JSON Mode或Structured Output。它的核心思路是在请求模型时明确要求返回JSON格式并告诉模型严格的JSON Schema模型会尽可能保证输出结构合法。有人可能会问函数调用返回的arguments也是JSON这不就是结构化输出吗区别在于定位不同。函数调用输出的JSON描述的是“意图和参数”它必须对应到一个可执行的函数上结构化输出描述的是“事实和结果”它可以是一个信息抽取结果、一份数据处理后的报告、一个Agent内部流转的状态对象。结构化输出最典型的应用场景是信息抽取和数据清洗。比如用户发来一段售后投诉你需要从中提取订单号、问题类型、用户情绪、期望解决时间这些字段要入库、要触发后续流程必须保证格式完全一致。用纯文本补全再解析那是一场灾难用结构化输出模型直接给你一个规范的JSON代码拿到就能用。4.2 强制JSON Schema的最小实现目前主流模型都支持结构化输出用起来很直接以下面这段为例from openai import OpenAI import json client OpenAI(api_keyyour-api-key) schema { name: extract_complaint_info, strict: True, schema: { type: object, properties: { order_id: {type: string}, issue_type: {type: string, enum: [物流, 质量, 售后, 其他]}, user_sentiment: {type: string, enum: [愤怒, 普通, 满意]}, expected_solution: {type: string} }, required: [order_id, issue_type, user_sentiment, expected_solution], additionalProperties: False } } response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个投诉信息抽取助手从用户留言中提取结构化信息。}, {role: user, content: 我上周买的手机今天到了发现屏幕有划痕气死我了订单号是ORD20240988我要退货} ], response_format{ type: json_schema, json_schema: schema } ) content response.choices[0].message.content data json.loads(content) print(data)注意两个关键点。一是strict模式开启后模型必须严格遵循schema不能多输出字段也不能少输出必填字段。这样代码里就不用写一堆容错逻辑。二是enum枚举值对约束模型非常有帮助把issue_type限制在几个固定值内后续程序按枚举值做决策不会遇到无法预料的字符串。4.3 结构化输出的限制与坑位虽然结构化输出很香但它也有自己的问题踩过的坑分享几个。一个是Schema过于复杂时模型仍然可能出错或响应变慢。我曾见过有人把一个业务对象的二十多个字段全部塞进JSON Schema嵌套四层深。结果是模型每次请求都要思考很久偶尔还会在某个内层字段上偷工减料。做法应当是单次请求只抽取最核心的字段复杂的结构化需求拆成多次模型请求完成。宁可多花一两次调用的成本也不要让一次请求背太多负担。另一个坑是结构化输出并不保证语义一定正确。模型可能给你一个合法的JSON但内容是胡编的。比如用户留言里根本没有订单号模型还是会“合理猜测”一个看起来很像的字符串。所以在关键字段上程序仍然需要做二次校验比如订单号是否符合格式、是否存在数据库中。结构化输出解决的是格式问题不是事实问题。还有一个容易忽略的点结构化输出和纯文本生成在token消耗上有差异。JSON格式本身要占用大量token同样的内容用结构化输出会比纯文本多花20%到50%的token。如果是高频调用场景这个成本差异要提前算清楚。5. 三种方式的实战对比与选型思路5.1 一张表看清三个核心维度的差异三种方式用下来把关键维度放到一起对比会更直观维度纯文本补全函数调用结构化输出输出形态自由自然语言函数名参数JSON符合Schema的JSON解析难度高依赖正则清理低直接取tool_calls低直接json.loads适合消费方人类阅读程序执行动作程序处理数据不确定性高中工具名和参数可能选错低格式可控典型场景摘要、回复生成、改写查询、操作、任务执行抽取、分类、状态流转Token消耗最低中较高实现复杂度最简单中等工具定义需设计中等Schema需维护从表里能看出一个趋势越往后的方式可控性越强但约束也越多成本和复杂度也越高。选型的核心不是选“最强”的而是选“恰好能覆盖你的场景约束”的。5.2 不同Agent场景的选型建议根据我接触过的多个Agent项目经验给你一套可以直接用的选型思路如果模型的输出是直接呈现给用户看的比如对话回复、内容总结、创意文案用纯文本补全。不要让模型为了配合程序而牺牲表达的自然度。如果Agent需要操作外部系统、查询数据、调用API用函数调用。这是Agent连接真实世界的关键通道。如果Agent需要把模型输出作为内部数据流转、落库、触发下游任务用结构化输出。比如意图识别、实体抽取、路由决策。如果同一个流程里既需要抽取数据又要调用工具可以先结构化输出完成数据抽取和意图判断再根据抽取结果决定调用哪个工具。混合使用往往才是最优解。我之前设计过一个客服Agent整体流程是这样的先用结构化输出从用户消息里抽取订单号、问题分类、紧急程度然后根据问题分类用函数调用去查订单、查库存、查物流最后用纯文本补全生成一段自然、有温度的回复。三个环节各用各的方式全程几乎没有因为解析问题返工过。5.3 混合调用的一个完整链路演示把上面的思路串成代码直观感受一下三种方式如何协同# 第一步结构化输出抽取意图 intent_schema { name: intent_extract, strict: True, schema: { type: object, properties: { category: {type: string, enum: [订单查询, 退货, 投诉]}, order_id: {type: string}, urgency: {type: string, enum: [低, 中, 高]} }, required: [category, order_id, urgency], additionalProperties: False } } # 请求模型获得意图JSON intent_response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 我的订单ORD20240988怎么还没发货挺着急的}], response_format{type: json_schema, json_schema: intent_schema} ) intent_data json.loads(intent_response.choices[0].message.content) # 第二步根据意图调用工具 if intent_data[category] 订单查询: result query_order_status(intent_data[order_id]) # 第三步把工具结果交给模型生成回复 reply client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是有耐心的客服助手用自然口语化方式回复用户。}, {role: user, content: f订单{intent_data[order_id]}的状态是{result}} ] ) print(reply.choices[0].message.content)这套链路跑起来非常稳定。第一步的输出具有严格格式程序可以放心做分支判断第二步把工具结果拼进提示词模型在明确上下文里生成最终回复自然度和准确性都有保障。比起让模型一口气完成理解、决策、表达三个任务单一环节的模型请求都简单了很多还更容易排查问题。6. 常见问题排查与避坑手册6.1 高频踩坑问题速查表实操过程中遇到的问题翻来覆去就那么几个整理成速查表照着排查能省不少时间现象可能原因处理思路模型返回的JSON解析失败Schema太复杂或约束不足简化Schema开启strict模式减少嵌套层级函数调用选错工具工具描述模糊、工具数量太多重写description分层设计工具用tool_choice强制指定调用工具后模型胡编结果没有把工具执行结果追加到消息历史把tool执行结果作为tool角色消息传回模型模型输出截断max_tokens设置太小调大max_tokens精简system提示词减少冗余响应特别慢上下文过长、Schema复杂、模型过大精简历史消息做上下文裁剪换更快的模型同一个问题每次都返回不同结果temperature过高把temperature调到0或接近0改用结构化输出约束参数提取错误参数description缺少格式示例在description里写示例值模型模仿能力很强6.2 排查问题的通用方法论排查Agent的问题最忌讳的就是靠感觉猜。很多同学遇到问题直接在代码里瞎试改来改去碰运气效率极低。我的做法是先建立全链路日志体系把每次模型请求和响应的完整内容都记录下来。每条日志至少包括请求入参的messages、tools或response_format配置、完整的模型返回消息包括content和tool_calls、后置解析结果、最终执行结果。不要只记最终输出那样回看的时候根本找不到问题出在哪一步。有一次排查一个订单Agent的Bug用户说“帮我取消订单”Agent回复“好的已经帮您取消”但实际上没有执行取消动作。翻日志发现模型其实返回了tool_calls调用取消接口但是由于我的解析代码判断字段写错了把工具调用忽略了然后模型又自己补了一句“已经取消”。这就是典型的模型“脑补”解决方案是在代码里严格校验如果意图是执行操作但tool_calls为空必须返回失败让模型重新理解不能放行。还有一个经验是启用结构化输出后仍然要保留一个兜底分支。因为极端情况下模型可能返回空content或不合法的JSON。我在生产代码里都有一个统一的解析函数JSON解析失败时自动重试一次并把错误信息拼进提示词让模型自我纠正。实测下来重试成功率相当高这是不用大改架构就能明显提升稳定性的手段。6.3 一个具体问题的排查过程参考有一次某物流Agent在深夜突然出现大量超时查询发现所有请求都在调用同一个“查询快递”工具。进一步看响应时间分布发现模型被传入了过长的快递公司列表每次都要在十几家公司里做选择还要额外思考是否要调用工具。优化方案很简单把快递公司列表从工具参数中移除改为热门公司写死在代码逻辑里。工具参数简单了模型决策快了很多超时问题消失。这类问题其实暴露了一个共性Agent里的一次模型请求往往承担了太多模糊的决策压力。你以为是在“调用模型”实际上是在逼模型同时完成格式解析、语义理解、工具选择、参数提取多项任务。每次出现问题先问自己这次请求是不是可以拆得更简单输出契约是不是更明确工具定义是不是更精简这三板斧用下来大部分稳定性问题都能找到根子。7. 写在最后的实用建议项目做得多了我越来越觉得模型调用方式的选择本质上是在给系统定规矩。规矩太松输出飘逸无法收敛规矩太紧模型表达空间受限复杂任务反而做不好。找到那个平衡点需要你对自己的场景有足够清晰的边界意识。每次开新需求我的第一件事不是写代码而是画一条数据流线用户的输入进来之后哪些环节模型必须输出结构化数据哪些环节需要模型决策调哪个外部能力哪些环节只是生成给用户看的话把这三个问题回答清楚三种方式的选型就自动浮出水面了。还有一点想提醒的是不要迷信某一个模型的“结构化输出能力”。不同模型对JSON Schema的理解和遵循程度差别很大哪怕严格模式也不是万事大吉。在一个新模型上线前拿你的真实数据跑一遍三种方式的对比测试看准确率、延迟、token消耗再把结果固化到选型文档里。这个测试过程会帮你少走非常多弯路。