最近帮两个团队做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_key="your-api-key") response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个简洁的摘要助手,只用3句话概括用户输入的核心信息。"}, {"role": "user", "content": "我们想做一个新的智能客服系统,希望能在用户咨询时自动判断问题分类,并给出初步解决方案。现有系统响应时间长,人工成本高。"} ], temperature=0.3, max_tokens=200 ) 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_key="your-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( model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="auto" ) 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_key="your-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( model="gpt-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( model="gpt-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( model="gpt-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消耗,再把结果固化到选型文档里。这个测试过程会帮你少走非常多弯路。
最近帮两个团队做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_key="your-api-key") response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是一个简洁的摘要助手,只用3句话概括用户输入的核心信息。"}, {"role": "user", "content": "我们想做一个新的智能客服系统,希望能在用户咨询时自动判断问题分类,并给出初步解决方案。现有系统响应时间长,人工成本高。"} ], temperature=0.3, max_tokens=200 ) 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_key="your-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( model="gpt-4o-mini", messages=messages, tools=tools, tool_choice="auto" ) 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_key="your-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( model="gpt-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( model="gpt-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( model="gpt-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消耗,再把结果固化到选型文档里。这个测试过程会帮你少走非常多弯路。