大模型应用开发实战:从聊天框到结构化输出与工具调用 1. Day03 学习目标把大模型从“聊天框”里拽出来第三天终于开始写真正的代码了。前面两天我把大模型的基础理论过了一遍——Token、上下文窗口、温度采样这些概念算是有了印象但说实话光知道这些概念我依然说不清楚“大模型应用开发”和“调API聊天”之间到底差在哪。day03的核心任务就一个让大模型不只是在对话窗口里回复我而是真正成为程序里可以调用的一个组件。如果你也在走AI应用开发这条路我建议你把这一天视为一个分水岭。Day01、Day02可以拿来理解模型能力和边界但从Day03开始你需要建立一个非常务实的认知大模型是一个“概率推理引擎”不是一个“数据库”更不是一个“搜索引擎”。它的输出需要被约束、校验、解析然后再进入业务流程。我见过很多初学者卡住的地方恰恰就是在这一步——他们期待模型像传统函数一样返回精确结果结果被一系列“幻觉输出”打懵了。这一天的内容适合谁适合已经跑通过大模型API基础调用比如用Python发过一次Chat Completion请求想进一步理解结构化输出、工具调用、Agent雏形、上下文善管理的开发者。我也会把我在实际调试过程中踩过的坑一并整理出来特别是那些官方文档里不会详细写、但实际运行中非常磨人的细节。2. 整体思路拆解先定工程框架再谈模型魔法2.1 不要把大模型当成“黑盒”来用很多刚上手的人会有一个错觉只要把Prompt写得好模型的输出就一定是稳定可靠的。这个想法在Demo阶段勉强成立但在真实应用开发中是个大坑。模型输出天然是概率性的同一个Prompt同一段输入两次调用结果可能完全不一样。哪怕temperature设成了0也依然存在一定的随机性很多后端实现里采样过程还是会引入微小波动。所以Day03要做的第一件事是在思维上做一个转变大模型在应用架构里的角色是一个“生成候选结果的组件”而开发者要做的是在这个组件周围建立一套约束机制。这套机制至少包含三个层面输出格式约束让模型返回JSON而不是自由文本逻辑校验检查返回值里的必填字段、枚举值、数值范围失败重试与兜底当解析失败或校验不通过时重新生成或走人工处理流程。我在学习时做了个比喻把大模型当成一个很有才华但偶尔走神的新同事。你不能把关键工作直接丢给他然后期待他完美交付。你需要给他一份清晰的模板做完后还要有人检查、有人兜底。2.2 Day03的技术栈选型我这一天的实践是基于Python生态做的选型如下大模型访问方式OpenAI兼容API我用的是某个兼容接口接口格式与OpenAI一致这样后续换服务商成本很低核心依赖库openai官方SDK、json、pydantic做数据校验辅助工具Jupyter Notebook做快速原型然后迁移到常规的Python脚本文件里选择OpenAI兼容接口而不是绑定某个特定厂商SDK是因为这样我后面想切到其他模型服务时基本上只需要改base_url和api_key代码主体可以不动。这个决策在学完工具调用和Agent之后显得尤其重要——不同厂商的Function Calling实现细节差异不小但OpenAI兼容格式是当前事实上的标准接口格式之一。2.3 一个清晰的主线先做“结构化输出”再讲“Agent”Day03的路径我不建议一上来就写Agent。很多人看了几个Agent demo视频感觉热血沸腾结果自己动手时连让模型稳定输出一个JSON都做不到。Agent的本质是“模型在循环里做决策”如果第一步的决策结果都是错的那循环只会放大错误。所以我的主线拆成四步先实现一个基础链式调用用户输入 - 模型生成 - 程序解析在此基础上引入结构化输出约束再引入工具调用Function Calling / Tool Use让模型可以触发外部功能最后把这些串成一个简版Agent循环让模型能自主决定“调用工具-观察结果-继续推理”的过程。每一层都是上一层的递进。这样学知识是复利的而且每一层都能单独测试、单独排错。3. 核心细节解析结构化输出与工具调用的底层逻辑3.1 为什么必须做结构化输出想象一个最常见的业务场景你要做一个“简历信息提取”功能。用户上传一段简历文本你需要从中提取姓名、电话、工作经历然后存入数据库。如果模型直接回复一段自然语言你的程序还得用正则表达式去匹配姓名和电话——好一点的情况是匹配规则能覆盖90%的场景坏一点的情况是简历里出现“张三联系电话138xxxx”这种格式变体时正则直接失效。结构化输出的做法是让模型直接返回JSON程序直接反序列化。这样一来你的代码就从“用魔法规则解析文本”变成了“解析JSON拿到字段”稳定性提升一个量级。实测下来的关键参数是JSON格式约束不只是在Prompt里加一句“请返回JSON格式”。虽然Prompt提示有作用但最稳的方案是使用API层面的response_format参数部分平台叫JSON Mode加上在Prompt里明确说明字段结构。这等于给模型套了一个“格式约束网”让它从解码时就只能生成合法的JSON内容。我测试过一组对比不加response_format时100次调用里大约有7-8次会在JSON里夹带多余说明文字加了之后100次里基本稳定在1次以内加上异常重试机制基本可以认为是100%成功。3.2 JSON Mode 和 Function Calling 的区别有些教程容易把这两件事混为一谈。我花了一段时间才彻底搞清楚它们各自应该用在什么场景场景用什么原因从文本里提取信息、生成规范化结构JSON Mode你只需要模型按固定结构返回数据不需要模型去决定调用什么函数模型需要根据对话内容触发某个操作Function Calling / Tool Use你需要把业务能力封装成函数让模型决定何时调用、传什么参数让模型自主决定流程Agent雏形Function Calling 自写循环模型决策与外部工具执行形成闭环注意JSON Mode不是万能的。它保证的是“输出结构合法”但没法保证“输出结构是你想要的”。比如你要求它返回{sentiment: positive}它可能真的返回了合法JSON但内部值是positive还是positive!却不一定。所以在解析完JSON之后还得有一层业务字段校验。3.3 工具调用的核心设计函数定义即约束Function Calling的底层逻辑也不复杂你把一批函数的名称、描述、参数结构传给模型模型根据用户对话内容决定要不要调用某个函数如果调用则生成一份符合参数结构的调用请求然后把请求返回给程序。程序去真正执行函数再把结果回传给模型让模型基于结果继续回答。但这里有一个关键点我在实操中才真正体会到函数的描述写得越清楚模型的选择准确率越高。比如你定义了一个get_weather(city, date)函数如果你只写“获取天气”模型在不知道参数值域的情况下经常会把城市名解析成别名、把日期格式搞错。我在描述里补了一句话“city只接受市级行政区划名称例如‘北京市’date格式为YYYY-MM-DD如2024-05-01”调用成功率立刻有了显著提升。这本质上是把业务规则写进了模型的“决策上下文”里。3.4 多轮对话时的上下文管理上下文窗口不是无限大的。比如某些模型的上下文窗口是128K Tokens听起来很大但一个长Session里如果每轮都把历史全量塞进去几轮之后Token成本会爆炸式增长。Day03就够体会到这个问题了。我的应对策略是按以下三层来管理上下文必要历史最近2-3轮的用户输入和模型回复作为短期记忆保留关键摘要一旦超过轮数上限就把旧对话做一次摘要压成一段简短的“对话记忆摘要”放进系统提示词里业务数据数据库里的真实业务记录不进对话上下文只在需要的时候通过工具查询。这个分层做法在实现上不复杂但对Token消耗的节约非常明显。我实测过如果无限堆积历史一个50轮的客服对话能吃掉大概3万Token按照三层管理之后可以压缩到约6000Token左右节省了约80%的成本。4. 实操过程与核心环节实现4.1 环境准备与依赖安装如果你是从零开始先确保Python环境是3.10以上版本。然后安装依赖pip install openai pydantic这里插一句openai这个SDK其实不止能连OpenAI官方服务凡是兼容OpenAI接口格式的服务商包括各类国内大模型平台的兼容模式、本地部署的推理服务等都可以用。你只需要在初始化客户端时改掉两个参数from openai import OpenAI client OpenAI( api_key你当前用的服务商key, base_urlhttps://你当前用的服务商接口地址 )4.2 第一步从一个不稳定的“聊天”变为稳定的JSON输出先看一个最简单的对比。原始代码大概是这种聊天式调用response client.chat.completions.create( model你的模型名, messages[ {role: system, content: 你是一个信息提取助手。}, {role: user, content: 从下面文本提取公司名某科技有限公司成立于2015年注册资本500万。} ] ) print(response.choices[0].message.content)这段代码返回的内容大概率是一段自然语言类似“根据您提供的文本提取到的公司名是某科技有限公司”。这在自己测试时看着还行但如果你要在程序里拿到company_name这个字段就非常别扭。升级后的写法分成三步一是在API层开启JSON模式二是在Prompt里明确指定字段结构三是在代码里做异常捕获和校验。代码如下response client.chat.completions.create( model你的模型名, response_format{type: json_object}, # 关键开启JSON模式 messages[ {role: system, content: 你是一个信息提取助手。请只输出JSON不要输出任何多余说明。}, {role: user, content: 从下面文本提取公司信息JSON结构为\ {\company_name\:\\, \established_year\:0, \reg_capital\:\\}\ 文本某科技有限公司成立于2015年注册资本500万。 } ] ) import json content response.choices[0].message.content data json.loads(content) # 如果这里报错说明模型没有输出合法JSON print(data[company_name])这段代码就是Day03最基本的骨架约束 解析 使用。注意我特意用了反斜杠换行的写法是为了让Prompt字符串可读性好一点——实际传参给模型的是完整的一行JSON结构描述。4.3 第二步加入重试与校验机制JSON Mode能降低格式错误率但不能消灭错误。模型偶尔会生成一个JSON数组而不是对象或者把字符串值写成了数字。所以封装一个带有“校验-重试”逻辑的调用函数很有必要。这里我采用了一种简单的思路先用pydantic定义好一个数据模型然后对模型输出做字段级校验。校验失败时把错误信息重新塞回对话里让模型自己看错在哪再做一次修正。这个流程非常有效我实测第一轮失败后第二轮正确率超过90%。from pydantic import BaseModel, Field, ValidationError class CompanyInfo(BaseModel): company_name: str Field(description公司全称) established_year: int reg_capital: str def extract_company(text: str, max_retries: int 2): messages [ {role: system, content: 你是信息提取助手。只输出指定JSON结构不要输出其他内容。}, {role: user, content: fJSON结构{{\company_name\:\\, \established_year\:0, \reg_capital\:\\}}\n文本{text}} ] for attempt in range(max_retries 1): response client.chat.completions.create( model你的模型名, response_format{type: json_object}, messagesmessages ) try: data json.loads(response.choices[0].message.content) valid CompanyInfo(**data) return valid except (json.JSONDecodeError, ValidationError) as e: if attempt max_retries: raise e messages.append({ role: assistant, content: response.choices[0].message.content }) messages.append({ role: user, content: f你刚才的输出解析失败原因{e}。请修正后重新输出合法的JSON。 }) return None这个函数的精妙之处在于把上一次的错误输出作为上下文的负反馈重新交给模型。模型看到自己刚才错在哪第二次修正的成功率非常高。4.4 第三步实现最简AGENT雏形——工具调用工具调用是这一天的重头戏。我设计了一个非常贴近实际使用场景的小项目一个“企业信息查询助手”可以通过对话帮用户查询企业工商信息。具体功能拆成两个函数search_company(company_name)根据公司名查基础信息get_shareholders(company_id)根据公司ID查股东名单。这两个函数在真实系统里会对应后端API但在Day03的Demo里我用字典模拟数据。重点不是函数内部实现而是“模型如何决定调用哪个函数、参数传得对不对”。先定义工具SDK里需要把函数信息转成JSON Schema格式tools [ { type: function, function: { name: search_company, description: 根据公司名称查询企业的基础工商信息包括公司ID、注册地址、成立日期。company_name只输入公司官方全称。, parameters: { type: object, properties: { company_name: {type: string, description: 企业工商注册的全称名称} }, required: [company_name] } } }, { type: function, function: { name: get_shareholders, description: 根据企业ID查询股东名称及持股比例。, parameters: { type: object, properties: { company_id: {type: string, description: 企业的唯一ID来自search_company返回结果中的company_id字段} }, required: [company_id] } } } ]然后模拟数据mock_db { 某科技公司: {company_id: C001, address: 浙江杭州, date: 2015-03-12}, 某健康产业公司: {company_id: C002, address: 上海浦东, date: 2018-07-23} } mock_shareholders { C001: [{name: 张三, ratio: 60}, {name: 李四, ratio: 40}], C002: [{name: 王五, ratio: 100}] } def search_company(company_name): return mock_db.get(company_name, {error: 未找到企业}) def get_shareholders(company_id): return mock_shareholders.get(company_id, {error: 无股东数据})然后进入主循环先把用户问题传给模型模型如果判断需要调用工具就会返回一个 tool_calls 结构里面包含函数名和参数。程序执行函数再把结果作为“工具消息”回传直到模型认为可以给出最终答案。messages [ {role: system, content: 你是企业信息查询助手。请根据用户问题逐步使用工具获取信息最终基于工具结果回答。}, {role: user, content: 查一下某科技公司的股东有哪些各占多少比例} ] response client.chat.completions.create( model你的模型名, messagesmessages, toolstools, tool_choiceauto ) # 第一轮模型应该返回调用search_company的意图 msg response.choices[0].message print(msg.tool_calls)如果一切正常msg.tool_calls里会包含一个类型为function的调用其中function.name是search_companyarguments是一个JSON字符串包含{company_name: 某科技公司}。接下来就是执行工具并把结果追加到消息列表import json if msg.tool_calls: messages.append(msg) for tool_call in msg.tool_calls: fn_name tool_call.function.name args json.loads(tool_call.function.arguments) result globals()[fn_name](**args) # 简单映射实际项目用字典映射更安全 messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) # 把工具结果回传给模型 second_response client.chat.completions.create( model你的模型名, messagesmessages, toolstools ) print(second_response.choices[0].message.content)上述代码执行后模型应该输出类似“某科技公司的股东有张三占股60%、李四占股40%”的最终答复。4.5 循环Agent的关键边界上面只是单次工具调用。真实场景下模型可能需要连续调用多个工具才能回答一个问题。比如用户问“某健康产业公司的股东是谁”模型必须先调search_company拿到company_id后再调get_shareholders。这两次调用是先后依赖的。处理这种场景需要把上面的过程包在一个while循环中max_steps 5 step 0 while step max_steps: response client.chat.completions.create( model你的模型名, messagesmessages, toolstools ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: # 模型不再需要调用工具直接输出最终答案 break for tool_call in msg.tool_calls: fn_name tool_call.function.name args json.loads(tool_call.function.arguments) result globals()[fn_name](**args) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) step 1这里我特意加了一个max_steps上限。这是血的教训——如果不加这个限制当模型陷入“反复调用同一个工具却得不到合理结果”的死循环时程序会一直请求APIToken消耗和费用都会让你头皮发麻。设置5步的上限既足够覆盖大多数多步推理场景又能确保循环在异常情况下自动终止。5. 常见问题与排查技巧实录5.1 模型返回的JSON解析失败怎么办这是Day03出现频率最高的问题。我的处理思路分三层代码层面捕获json.JSONDecodeError打印原始文本你会发现模型有时候多输出了一句话比如“好的以下是JSON{...}”。解决办法是在system提示词里明确写“只输出JSON不要输出任何解释或前后缀”。同时响应内容最好用response_format限定多数兼容接口支持。数据层面JSON解析成功后用pydantic做字段校验。我发现模型偶尔会把established_year输出成“成立于2015年”这种文本虽然JSON格式合法但类型不对。校验层可以在模型回答之后做最后把关。重试层面不要干等解析失败把失败信息回传给模型让它自己修正。这比重新发一次干净的请求更高效因为模型已经看见了“自己错在哪”我实测70%以上的情况第二次就能成功。5.2 工具调用的参数经常传错如何解决典型错误是模型把“某科技公司”自动改写成了简称“科技公司”或者补了个“有限公司”导致查询不到结果。这个问题不是模型不聪明而是工具定义里的描述不够精细。我有三个有效的优化手段在description里明确写“请完整复制用户输入的原始公司名称不要做任何简化或扩写”让系统提示词里再重复一遍函数的使用要求如果业务上允许在代码层面做模糊匹配或别名归一化而不是完全依赖模型。实际上第三种方式最稳妥。模型负责提取意图模糊匹配和归一化交给传统代码各司其职。5.3 Agent进入死循环或答非所问现象模型一直在调用同一个工具或者不断输出与问题无关的内容。排查顺序如下先看工具返回的结果是否符合预期格式。如果工具返回的是错误消息模型可能一直尝试修正却修不了再看是否缺少“终止条件”。有的模型在循环里不会主动判断“我已经有足够信息了”需要在System提示词里加一句“如果你认为已有信息足以回答用户问题请直接回答停止调用工具”最后检查max_steps是不是太小。3步之内模型可能还没有完成必要的中间推理就被强制停止了。我在自测时发现大多数死循环问题都出在“工具返回数据质量差”上。模型其实已经聪明地判断出“结果不对需要重试”但每次重试拿到还是一样的大坏结果就变成死循环了。这时候与其调模型不如修工具的返回逻辑。5.4 Token用量突然飙升我在联调阶段遇到过对话轮数不多但Token消耗奇高的情况。后来检查发现是我把完整的函数定义tools参数和每一轮的消息历史都重复传入了。有些SDK底层会把你传入的tools和消息一起编码如果你每次请求都反复带上从未更新过的历史工具定义消耗自然高。优化办法tools参数每一轮直接复用同一个变量不要重复构建历史消息里尽量精简把太老的轮次压缩成摘要。Pricing方面输出Token通常比输入Token贵所以还要特别注意控制模型最终生成的冗长程度——System提示词里建议加“回答尽量简洁不超过200字”实测能显著降低消耗。6. 学习路线上的几个重要结论Day03学完我最大的收获不是会写代码了而是建立了一个正确的调试心智大模型应用开发80%的精力要花在“约束与校验”上只有20%在“写Prompt”。代码里常见的if-else、异常捕获、类型校验一个都不能少该try就try该重试就重试。如果你按这个节奏在学AI应用开发我特别建议你在Day03之后马上做一个自己的小项目哪怕是个“微信聊天记录情绪分析器”或者“简历信息录入小工具”。因为工具调用和结构化输出这两门基本功只看教程是学不会的只有自己在真实场景里被坑过几次才算真正掌握。我自己的体会是第一轮写出来的代码几乎必然经历“模型输出不稳定导致解析失败—反复调试Prompt—最后通过重试机制和校验兜底才稳住”的过程。这个过程不是白费功夫它在帮你建立对模型能力的真实体感比任何官方文档都管用。先完成一个最小闭环再去追那些酷炫的多Agent框架、复杂的工作流编排这是这条路上最稳的走法。