首页 / 资讯中心 / 文章详情

AI应用开发实战:结构化输出、Function Calling与上下文管理

AI应用开发实战:结构化输出、Function Calling与上下文管理 ★ FEATURED ARTICLE
昨天深夜总算把大模型的API Key调通今天一整天都在跟“怎么让模型输出程序能直接用的东西”较劲。这是AI应用开发学习的第二天我想把今天的实战过程记录下来尤其是几个绕不开的核心环节结构化输出、Function Calling、多轮对话的上下文管理。说白了昨天的感受是“模型终于能说话了”今天的感受是“模型能不能按我的规矩干活”。如果你也刚接触AI应用开发正卡在“模型会聊天但不会干活”的阶段这篇记录应该能帮你省点时间。先说一下背景。我的工具栈比较简单本地Python 3.10 虚拟环境OpenAI兼容接口的大模型SDK配合一个带JSON模式和函数调用支持的模型服务。文章不依赖具体哪家厂商核心思路都基于兼容OpenAI协议的通用接口换到其他家时只需要查一下参数名对应关系就行。适合有Python基础、没系统接触过AI应用开发的读者。1. 第一天复盘与第二天规划1.1 第一天的真正收获不是hello world而是环境敏感度先简单回顾第一天。很多人以为第一天就是“跑通一个hello world”实际动手才发现难点在别处。首先是Python版本管理电脑上装了好几个PythonSDK版本一升级就报错最后用虚拟环境才把依赖隔离干净。其次是API Key的权限范围创建时如果没勾选对应的模型权限调用直接返回401。这两件事看着琐碎却让我对AI应用开发有了清醒认识模型能力是别人的周围所有工程问题都是你的。这种环境敏感度第二天马上就用上了。今天调试Function Calling时我一开始怀疑是自己的参数写错了后来打印完整请求日志才发现问题出在模型服务对tools参数的大小写校验上。如果没有第一天养成的日志排查习惯这一步可能要卡两个小时。1.2 今天的学习目标让输出从“人看得懂”变成“程序用得上”第二天的目标非常明确让模型的输出从“人看得懂”升级为“程序用得上”。我给自己拆了三个小目标结构化输出要求模型在指定场景下返回JSON字段固定、可校验工具调用让模型在合适的时候触发我注册的函数而不是凭空编答案上下文管理让多轮对话保持连续同时控制Token成本。这三个目标正好对应AI应用开发的三个基本功做完它们我的Demo才算靠近“能被别人试用”的状态。其实这三个目标有一条暗线每一步都在把“不确定的模型行为”往“确定的程序行为”上推。结构化输出是数据层面的确定化工具调用是行为层面的确定化上下文管理是状态层面的确定化。理解了这条暗线今天学下来就不会觉得只是在抄代码。2. 从裸调API到工程化调用我放弃了哪些“捷径”2.1 最小调用可以有多小比你想象的还小把API调用写成能跑的最小代码其实非常简单。用兼容OpenAI协议的SDK核心就几行from openai import OpenAI client OpenAI( api_keysk-..., base_urlhttps://..., ) resp client.chat.completions.create( modelyour-model, messages[{role: user, content: 你好}], ) print(resp.choices[0].message.content)但如果你把这个代码直接上生产会被自己坑死。我今天把它扩展成了一个带环境变量、超时、重试和日志的最小模板代码长了不少但心里踏实import os import time from openai import OpenAI, APIError, APIConnectionError, RateLimitError client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), timeoutfloat(os.getenv(LLM_TIMEOUT, 30)), ) def chat_with_retry(messages, max_retries3): for attempt in range(max_retries): try: resp client.chat.completions.create( modelos.getenv(LLM_MODEL, your-model), messagesmessages, temperature0.3, ) return resp.choices[0].message.content except RateLimitError: time.sleep(2 ** attempt) except APIConnectionError: time.sleep(2 ** attempt) except APIError as e: print(f[APIError] {e}) time.sleep(2 ** attempt) raise RuntimeError(模型调用失败) if __name__ __main__: print(chat_with_retry([{role: user, content: 用一句话介绍你自己}]))这个模板的核心思路有两条。第一所有外部依赖都可能失败所以要给重试机制而且要区分限流、网络错误和普通API错误因为限流等几秒自己就恢复网络错误可能要多等一会。第二所有失败都要留日志方便排查。没有日志的大模型调用就像没有黑匣子的航班出了问题你连复现都做不到。2.2 裸调API和产品级调用差在哪很多人会调API之后就觉得掌握了AI应用开发我以前也这么想今天被现实教育了。裸调API和产品级调用之间的差距至少体现在五个方面错误处理网络抖动、限流、模型服务异常每一类都要有对应策略超时控制不设置超时用户可能白等一分钟设置了超时就要想清楚超时后是重试还是降级格式约束自由文本输出对程序是灾难必须用参数加提示词双重锁死上下文管理没有上下文的多轮对话等于失忆可观测性模型输入输出要留日志否则出了问题连复现都做不到。这五件事单独看都不难但叠在一起就是AI应用开发真正的日常。模型是别人的工程是你自己的。这句话今天我在笔记上写了三遍每踩一个坑都想再写一遍。2.3 提示词是软约束参数是硬约束今天做结构化输出时我踩了一个有意思的坑。先在system prompt里写了“你必须返回JSON不要返回其他内容”结果模型偶尔还是会在JSON前后加一句“好的这是您要的结果”。后来我把temperature调低又启用了response_format的JSON模式这种情况才彻底消失。这件事给我的启发是提示词是软约束模型可能不听话参数是硬约束在接口层就锁死了输出格式。正确做法是双管齐下但把参数约束当主要手段。特别是你自己开发时一定要去翻模型服务的API文档搞清楚它支持哪些硬约束参数别只会在提示词里软磨硬泡。很多时候一条参数比十句提示词都管用。这里我顺手整理了一个参数速查表方便后面写代码时对照参数作用建议值temperature控制随机性值越大越有创造性0~0.3结构化任务直接设为0response_format强制输出结构json_object视模型服务而定max_tokens限制输出长度根据业务需求预留余量top_p采样范围一般与temperature二选一调整0.9~1.03. 结构化输出让模型从“会说”到“说对”3.1 为什么要逼模型输出JSON我今天要做的第一个小功能是“客服意图识别”。用户输入一句话程序要判断它属于“咨询”“下单”“投诉”“闲聊”中的哪一类还要抽关键实体比如商品名、订单号、时间。如果没有结构化输出模型给的自然语言根本没法直接解析程序就得靠正则去猜猜一次错一次。最自然的解法是要求模型输出JSON例如{ intent: 投诉, entities: { order_id: A20240501, issue: 物流延迟 }, confidence: 0.92 }有了这个结构程序能直接读intent字段走对应业务流程。这就是“从会说到说对”的含义不需要从文本里猜模型直接给你程序能消费的数据。这一步是后面一切高级功能的地基。3.2 如何让JSON输出更稳首先尽量用模型接口自带的结构化输出能力。在OpenAI风格的接口里就是response_format参数response client.chat.completions.create( modelyour-model, response_format{type: json_object}, messages[ {role: system, content: 你是客服意图识别助手。必须输出JSON包含intent、entities、confidence三个字段。intent只能是咨询、下单、投诉、闲聊之一。}, {role: user, content: 我想问一下你们的退货政策是什么} ], temperature0, )再加上几个细节稳定性会更好system prompt里列清枚举值给出一个JSON示例让模型照着填temperature设成0或0.1减少随机性解析后做字段校验缺字段就触发重试。这些细节单独看都很小合在一起效果明显。上午我在没做约束时JSON解析成功率大概只有六成加了这些约束后实测超过九成剩余的失败大多是因为输入本身太模糊模型不知道该填什么。3.3 解析模型输出时的三层兜底就算模型输出了所谓JSON也不要直接信任要做三层兜底。第一层是宽容解析先去首尾的反引号、markdown标记和多余文字再尝试json.loads。第二层是字段校验检查必填字段是否存在、类型是否对缺失时根据上下文补默认值或者触发一次重试。第三层是业务兜底如果重试还失败别让程序崩溃而是返回一个“未识别”结果产品层走人工接管流程。这三层兜底本质上是在承认一个事实模型输出永远有概率不可控。产品设计必须考虑这个概率而不是假装它不存在。这也是AI应用开发和传统后端开发的一大差异——传统后端可以假设上游接口基本稳定AI应用却天生要面对一个“会发挥失常”的上游。4. Function Calling让模型拥有“手”和“眼睛”4.1 模型不执行函数它只是调度器下午开始做Function Calling。一句话解释它你给模型一份工具清单模型在回答问题时可以选择调用某个工具并给出参数但真正执行函数的是你的代码执行完再把结果回传模型基于真实结果组织最终回答。我第一次理解这个逻辑时挺震撼的。因为这意味着模型不只是文本生成器它变成了一个决策者它知道自己不知道什么然后请求工具去查。再往后走一步就是Agent——只要在外面套一个循环让模型反复执行“决策-调用-观察-再决策”它就从一个回答问题的助手变成了一个主动干活的智能体。今天能把这一步跑通是最大的成就感来源。4.2 完整的最小示例天气查询我做的第一个工具是get_weather。完整代码大概这样import json from openai import OpenAI client OpenAI(api_key..., base_url...) tools [ { type: function, function: { name: get_weather, description: 当用户询问天气或出行建议时调用此工具查询指定城市指定日期的天气情况。, parameters: { type: object, properties: { city: {type: string, description: 城市名例如北京、上海}, date: {type: string, description: 日期格式YYYY-MM-DD默认今天} }, required: [city] } } } ] messages [{role: user, content: 北京今天会下雨吗}] response client.chat.completions.create( modelyour-model, messagesmessages, toolstools, ) msg response.choices[0].message if msg.tool_calls: func_call msg.tool_calls[0] function_name func_call.function.name arguments json.loads(func_call.function.arguments) print(模型想调用:, function_name, arguments) # 你的代码真正去执行这个函数 result {city: arguments[city], weather: 多云转阴, rain: False} # 关键一步把带tool_calls的assistant消息追加回历史 messages.append(msg) messages.append({ role: tool, tool_call_id: func_call.id, content: json.dumps(result, ensure_asciiFalse) }) final client.chat.completions.create( modelyour-model, messagesmessages, toolstools, ) print(final.choices[0].message.content)有几个细节一定要特别注意。第一把带tool_calls的assistant消息追加回messages时要追加整个msg对象不能只追加content因为模型要根据tool_call_id把工具结果和对应调用关联起来。第二最后一轮生成最终回答时tools参数仍然要带上否则模型可能在总结时失去工具上下文说出一些没着落的话。第三arguments是字符串要json.loads解析但这一步要加异常处理因为模型偶尔生成的参数确实不合规解析失败时应该给模型回一条错误消息让它重新生成。4.3 工具描述就是给模型写的“岗位说明书”今天最大的认知收获是这句话工具描述的质量决定了Agent的智商上限。模型会像新员工一样去读你的岗位说明书。你的说明书写得模糊它在边界情况就会脑补你把触发场景、参数含义、反面示例都写清楚它就能精准判断。举个例子。定义get_order_status工具时如果只写“查询订单状态”用户说“我的快递到哪了”模型有概率不调用因为用户没提“订单状态”。如果描述改成“当用户询问快递、物流、配送、订单进展时调用此工具参数order_id从用户消息中提取”模型几乎每次都能正确触发。这就是岗位说明书的差别。所以别心疼那点token工具描述值得用好文字。5. 多轮对话与上下文管理记忆是花钱买的5.1 Token开销是AI应用最大的隐形杀手多轮对话是AI应用最常见的交互形态但大模型本身没有记忆。所谓记忆就是你把历史消息再次发给模型。整个会话越长单次请求的Token就越多费用也越高。今天我做了一个小测试一个连续聊了30轮的会话最后一次请求光是历史消息就消耗了将近4000Token。假设这个应用每天有1000个用户每个用户平均聊30轮光上下文成本就能把预算打穿。所以上下文管理不是锦上添花而是上线前必须解决的问题。而且这个问题越到后期越难改因为它和业务逻辑纠缠得很深。5.2 我目前采用的三种策略今天我实现了一个简单的上下文管理器思路分三层。第一层窗口截断只保留最近N轮消息最旧的直接丢弃。优点是简单、零额外成本缺点是会“断片”用户明显感觉到系统忘了前面聊过的东西。第二层摘要压缩当历史超过阈值时让模型把旧消息生成一段摘要塞进system prompt再保留最近几轮原文。体验更好但每做一次摘要也要消耗Token相当于拿小开销换大开销。第三层关键信息提取针对业务场景从对话里提取用户偏好、订单号、待办事项等结构化信息单独存进业务数据库。对话轮数可以大胆截断因为这些关键信息已经“绕开”了模型上下文。第三种是我觉得最适合产品落地的思路它把记忆从模型上下文挪到了业务层稳定且可控。这三种策略不是互斥的实际产品里经常组合用前20轮用窗口之后触发摘要同时业务数据持续落地。我把它们整理成一张速查图方便取舍策略优点缺点适用场景窗口截断实现简单、零成本失忆明显轻量对话、Demo摘要压缩体验较好额外Token消耗中长会话关键信息提取稳定可控需要业务建模客服、电商、助手5.3 别忽视用户修改消息带来的上下文污染多轮对话还有一个很刁钻的问题用户修改自己之前的消息。比如先问“北京明天天气”过一会儿说“算了改问上海”。如果你简单地把两条用户消息都放进历史模型看到的是两个自相矛盾的请求很容易给出混乱回答。我的处理方案是在应用层做消息规范化同一意图的后续消息用新消息覆盖旧消息对应的槽位不留历史负担。再加上一条规则——一旦检测到用户说“算了”“改为”“重新来”就优先处理最新消息历史里的同主题消息标记为失效。这个逻辑看起来是业务层面的小事但实际效果比很多提示词技巧都明显。6. 没有Agent框架我先把裸流程跑了一遍6.1 我为什么排斥“第一天就上框架”现在打开社交平台到处是Agent框架教程有些甚至说“几分钟搭建一个智能体”。我承认这些工具确实提高效率但我仍然建议学习阶段先裸写一遍。原因很简单框架帮你掩盖了太多关键细节。比如工具调用的消息回传机制、tool_call_id的关联、多轮循环的终止条件这些东西你没在原生层跑过遇到问题就只能靠猜。今天我用原生SDK写了一个最简单的循环Agent模型返回tool_call就执行工具、回传结果直到模型给出最终回答。这个循环只有十几行却是所有Agent框架的基础。搞懂它之后再去看那些可视化平台你会发现它们本质上就是把“我手写的这个小循环”封装成了节点。这不是否定框架而是说框架不能替代理解。6.2 一个简单的Agent伪代码把今天写的东西抽象一下一个最小Agent循环大概是这样的while True: resp client.chat.completions.create( modelmodel_name, messagesmessages, toolstools, ) msg resp.choices[0].message messages.append(msg) if not msg.tool_calls: break for tool_call in msg.tool_calls: result execute_tool(tool_call.function.name, json.loads(tool_call.function.arguments)) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) })这个循环的核心就四步问模型、等判断、执行工具、回传结果。实际生产里要加的东西很多比如循环上限、单次执行超时、工具执行失败的回传格式、并发工具调用的结果合并。但骨架就是这个样子。所谓多Agent协作无非是让循环里的某个工具变成“发起对话的另一个Agent”。理解了这一点AI Agent的底层也就穿破了。6.3 给Agent学习者的路线建议我给自己整理了一条学习路线目前看是顺的先用原生SDK跑通一次单轮API调用理解messages和参数跑通结构化输出确保程序能稳定消费模型结果跑通单工具调用理解tool_call的消息回传机制跑通多工具调用注意多个tool_call的执行顺序和结果合并写一个Agent循环加入最多迭代N次的终止条件最后再学一个主流Agent框架用它重构自己的Demo。走完这条路线你不会成为Agent专家但会比那些只会拖拽节点的人更清楚系统在每一步到底发生了什么。遇到问题的时候你能直接定位到是模型判断错了、工具参数错了、还是消息回传错了而不是对着框架日志一头雾水。7. 第二天的一些真心话今天从早上九点写到晚上十点多顺畅的时候很快卡住的时候也真的很烦躁。最后不做什么宏大总结就说几句掏心窝的话。第一AI应用开发的难点不在模型而在工程。你要面对的不是“模型不够聪明”而是“模型输出不稳定、Token成本不可控、上下文中途失真”这些现实问题。它们的解法靠的是扎实工程习惯不是一句魔法提示词。第二学习新东西要用“最小闭环”验证。今天我不是一口气把Agent学完而是先结构化输出、再工具调用、最后才连成循环。每完成一步都有一个能跑通的代码心里就踏实一分。这种正反馈对自学者来说比任何学习计划都重要。第三别迷信“几分钟搞定Agent”的标题党。真要交付一个稳定的AI应用prompt、工具描述、上下文策略、兜底逻辑、可观测性一样都省不掉。这些东西没有一样能被拖拽完成。明天我打算把这些模块组装成一个带记忆、能调用简单工具的客服Demo把今天的代码串成一个小而全的作品。这篇文章就记到这里如果对你有帮助或者你也在这条路上踩了坑欢迎留言聊聊我们互相省点时间。
阅读完成 · 觉得有帮助?
咨询建站