AI大模型多轮对话与Function Calling实战:构建智能交互应用

📅 2026/7/6 9:21:39
AI大模型多轮对话与Function Calling实战:构建智能交互应用
1. 项目概述从单轮问答到智能交互的跨越在AI大模型的应用开发中我们常常会遇到一个瓶颈模型似乎很聪明能回答单次提问但一旦涉及需要多步骤、依赖上下文或需要调用外部工具来完成的任务时就显得力不从心。比如你想让AI帮你订一张机票它可能需要先询问你的出发地、目的地、时间然后查询航班信息最后完成支付。这个过程不是一次问答就能解决的它需要模型记住对话历史并在合适的时机调用外部API。这正是“多轮对话”与“Function Calling”技术要解决的核心问题。实战篇8我们就来深入拆解如何将这两项技术结合起来构建一个真正能“干活”的智能体。简单来说多轮对话让AI具备了“记忆力”和“逻辑连贯性”而Function Calling则赋予了AI“动手能力”。前者确保对话不会像金鱼一样只有7秒记忆后者则让AI不再只是空谈理论而是能真正操作数据库、发送邮件、控制设备。对于开发者而言掌握这两项技术意味着你能从构建“问答机”升级到构建“智能助手”甚至“自动化代理”。无论是客服机器人、智能家居中枢还是企业内部的工作流自动化工具其核心交互逻辑都建立在这两项技术之上。接下来我将结合具体的代码实例和架构设计带你一步步实现这个能力。2. 核心架构设计对话状态管理与工具调用的融合要理解多轮对话与Function Calling的结合首先得抛开对聊天接口的简单认知。我们不能再把每次用户输入都看作一个独立的请求而应该将其视为一个持续会话中的一次事件。这个会话拥有一个“状态”这个状态记录了历史消息、用户意图、以及等待执行或已经执行的工具调用结果。2.1 会话状态机的设计一个健壮的多轮对话系统其核心是一个状态机。这个状态机管理着整个对话的流程。我通常将其设计为几个关键部分对话历史存储器不仅仅是存储用户和AI的对话文本更重要的是存储每条消息的“角色”user, assistant, tool和“内容”。对于工具调用还需要存储工具调用的ID、名称、参数和返回结果。这部分数据是模型进行下一轮推理的上下文依据。意图与状态追踪器这不是一个显式的模块但其逻辑贯穿始终。我们需要通过模型的输出或自定义逻辑来判断当前对话处于哪个阶段。例如在订票场景中状态可能是“收集出发地”、“收集目的地”、“确认时间”、“查询航班”、“等待用户选择”、“执行支付”。Function Calling的引入使得“查询航班”和“执行支付”这两个状态可以直接由模型发起工具调用来触发。工具注册与执行器这是Function Calling的舞台。你需要一个中心化的“工具箱”向大模型声明你有哪些工具可用每个工具叫什么名字、描述是什么、需要哪些参数。当模型决定调用某个工具时它会输出一个结构化的调用请求你的系统需要解析这个请求找到对应的本地函数或API传入参数执行并将结果格式化后返回给模型供其生成下一步的回复。一个常见的架构流程是这样的用户输入 - 结合历史会话拼装完整Prompt - 调用大模型API - 模型返回普通文本或工具调用请求 - 如果是工具调用则执行工具 - 将工具执行结果作为新消息加入历史 - 再次调用大模型生成面向用户的回复 - 输出给用户。这个过程可能会循环多次直到任务完成。注意这里有一个关键设计抉择是否让模型在一次回复中既调用工具又生成文本主流做法是分离的。即模型在一次响应中要么返回纯文本要么返回一个或多个工具调用请求。执行工具后需要将结果再次提交给模型由模型来消化结果并生成最终的用户回复。这保证了逻辑的清晰和可控。2.2 工具Function的定义规范Function Calling的本质是让模型输出一个符合特定格式的JSON来触发你预先定义好的函数。因此工具的定义至关重要。以OpenAI的API为例你需要在请求时通过tools参数传入一个列表列表中的每个元素描述一个工具。{ type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气情况。, parameters: { type: object, properties: { location: { type: string, description: 城市名称例如北京、San Francisco }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位, default: celsius } }, required: [location] } } }这里的description字段极其重要它是模型决定是否以及如何调用该工具的主要依据。描述必须清晰、准确说明工具的用途和参数的意义。parameters遵循JSON Schema规范定义了参数的类型、是否必需、枚举值等。定义得越细致模型调用得就越准确。实操心得在定义工具时我习惯采用“高内聚、低耦合”的原则。一个工具只做一件事并且把事情做好。比如不要把“查询天气”和“查询航班”合并成一个工具。同时工具的描述要像给一个新手程序员写API文档一样避免歧义。曾经因为把参数date的描述简单写成“日期”导致模型有时传入“明天”有时传入“2024-05-27”给后端解析带来了麻烦。后来改为“日期格式为YYYY-MM-DD例如2024-05-27”问题就迎刃而解。3. 实战演练构建一个智能订餐助手让我们通过一个完整的例子将理论落地。我们要构建一个能处理多轮对话、可以调用外部函数的订餐助手。它的能力包括查询餐厅、查询菜品详情、加入购物车、下单。3.1 环境准备与工具定义首先假设我们使用OpenAI的GPT-4系列模型作为核心。我们需要安装OpenAI的Python SDK。pip install openai接下来我们定义三个核心工具函数及其对应的模型声明。注意这里是后端的真实函数和给模型看的“声明”是分开的但需要保持name字段一致。# 工具函数实现 def search_restaurants(location: str, cuisine: str None): 根据位置和菜系搜索餐厅。 Args: location: 地理位置例如“北京市海淀区”、“上海陆家嘴”。 cuisine: 可选菜系例如“川菜”、“意大利菜”。 Returns: str: 餐厅列表的格式化字符串。 # 这里应该是真实的数据库或API查询我们模拟一下 restaurants [ {name: 川味坊, cuisine: 川菜, rating: 4.5}, {name: 披萨大师, cuisine: 意大利菜, rating: 4.3}, ] filtered [r for r in restaurants if cuisine is None or r[cuisine] cuisine] result f在{location}找到{len(filtered)}家餐厅\n for r in filtered: result f- {r[name]} ({r[cuisine]}), 评分{r[rating]}\n return result def get_menu(restaurant_name: str): 获取指定餐厅的菜单。 Args: restaurant_name: 餐厅名称。 Returns: str: 菜单的格式化字符串。 # 模拟数据 menus { 川味坊: 水煮鱼 (68元)\n麻婆豆腐 (32元)\n回锅肉 (48元), 披萨大师: 玛格丽特披萨 (65元)\n海鲜披萨 (88元)\n意大利面 (45元), } return menus.get(restaurant_name, f未找到餐厅 {restaurant_name} 的菜单。) def place_order(items: list, delivery_address: str): 下单。 Args: items: 菜品列表每个元素是一个字符串格式为“菜品名 x 数量”。 例如[水煮鱼 x 1, 麻婆豆腐 x 2] delivery_address: 配送地址。 Returns: str: 订单确认信息。 # 模拟下单逻辑 order_id fORD{hash(str(items)delivery_address) % 10000:04d} total len(items) * 50 # 模拟计算总价 return f订单已创建订单号{order_id}。商品{, .join(items)}。配送至{delivery_address}。预估总价{total}元。请确认支付。然后我们创建对应的工具声明用于在调用ChatCompletion API时传给模型。# 工具声明给模型看的 tools_declaration [ { type: function, function: { name: search_restaurants, description: 根据用户提供的地理位置和偏好的菜系搜索附近的餐厅。, parameters: { type: object, properties: { location: {type: string, description: 详细的地理位置如街道、商圈或区域名称。}, cuisine: {type: string, description: 菜系类型如中餐、西餐、日料、川菜等。} }, required: [location] } } }, { type: function, function: { name: get_menu, description: 获取某个特定餐厅的详细菜单包括菜品名称和价格。, parameters: { type: object, properties: { restaurant_name: {type: string, description: 餐厅的完整名称。} }, required: [restaurant_name] } } }, { type: function, function: { name: place_order, description: 根据用户选择的菜品和配送地址创建订单。, parameters: { type: object, properties: { items: { type: array, items: {type: string}, description: 要订购的菜品列表每个元素格式建议为菜品名 x 数量。 }, delivery_address: {type: string, description: 详细的食品配送地址。} }, required: [items, delivery_address] } } } ]3.2 多轮对话循环与状态管理现在我们来编写核心的对话循环逻辑。这个循环需要维护消息历史并处理模型可能返回的工具调用。import openai import json # 初始化客户端和消息历史 client openai.OpenAI(api_keyyour-api-key) conversation_history [ {role: system, content: 你是一个友好的订餐助手可以帮助用户搜索餐厅、查看菜单并下单。请根据用户需求在必要时使用提供的工具。如果用户的信息不完整请礼貌地询问。} ] def run_conversation(user_input): # 1. 将用户输入加入历史 conversation_history.append({role: user, content: user_input}) # 2. 首次调用模型传入完整历史和工具声明 response client.chat.completions.create( modelgpt-4-turbo-preview, # 或 gpt-3.5-turbo messagesconversation_history, toolstools_declaration, tool_choiceauto, # 让模型自己决定是否调用工具 ) response_message response.choices[0].message # 3. 将模型的回复可能是文本也可能是工具调用请求加入历史 conversation_history.append(response_message) # 4. 检查模型是否想调用工具 tool_calls response_message.tool_calls if tool_calls: # 5. 处理每一个工具调用 for tool_call in tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) # 根据函数名调用对应的本地函数 if function_name search_restaurants: function_response search_restaurants( locationfunction_args.get(location), cuisinefunction_args.get(cuisine) ) elif function_name get_menu: function_response get_menu( restaurant_namefunction_args.get(restaurant_name) ) elif function_name place_order: function_response place_order( itemsfunction_args.get(items), delivery_addressfunction_args.get(delivery_address) ) else: function_response f错误未知函数 {function_name} # 6. 将工具执行结果作为一条新消息加入历史 # 注意角色是“tool”并且必须包含 tool_call_id conversation_history.append({ role: tool, tool_call_id: tool_call.id, content: function_response, }) # 7. 再次调用模型让它基于工具执行结果生成面向用户的回复 second_response client.chat.completions.create( modelgpt-4-turbo-preview, messagesconversation_history, ) assistant_message second_response.choices[0].message # 将模型最终的文本回复加入历史 conversation_history.append(assistant_message) return assistant_message.content else: # 模型直接返回了文本回复 return response_message.content # 模拟对话 print(run_conversation(我在北京国贸附近想吃川菜。)) # 输出可能调用 search_restaurants然后模型回复“在国贸附近找到一家‘川味坊’评分4.5。需要我为您查看它的菜单吗” print(run_conversation(好的看看菜单。)) # 输出可能模型知道上下文会调用 get_menu(restaurant_name川味坊)然后回复菜单详情。 print(run_conversation(我要一份水煮鱼和两份麻婆豆腐送到国贸大厦A座1001。)) # 输出可能调用 place_order然后回复订单确认信息。这个run_conversation函数是一个简化的核心循环。在实际生产中你需要将其封装成一个类更好地管理会话ID、持久化历史记录、处理并发等。实操心得tool_call_id非常重要。当模型一次请求调用多个工具时每个工具调用都有一个唯一的ID。你必须确保将工具执行结果的消息的tool_call_id设置为对应的ID这样模型才能知道哪个结果对应哪个请求。此外工具执行结果content字段最好是清晰的文本便于模型理解。如果返回的是复杂的JSON模型可能无法有效提取信息。4. 高级技巧与避坑指南掌握了基础流程后我们来看看如何优化和规避常见问题。4.1 引导模型与工具选择策略有时候模型可能不会主动调用你认为该调用的工具。这通常有几个原因工具描述不够清晰、用户query意图模糊、或者模型判断当前信息不足。你可以通过以下方式引导优化系统提示词在system消息中明确指示。例如“你是一个订餐助手当用户提到位置想找吃的时你应该主动使用‘search_restaurants’工具。当用户对某个餐厅感兴趣时使用‘get_menu’工具。”使用tool_choice参数如果你非常确定下一步必须调用某个工具可以将tool_choice参数设置为{type: function, function: {name: specific_function_name}}强制模型调用该工具。但大多数情况下“auto”是更好的选择。在对话历史中做示范在system消息或最初的user/assistant消息中给出一个调用工具的示例对话让模型通过few-shot learning来学习。4.2 处理复杂参数与错误模型生成的参数可能不完全符合你的预期。比如在订餐例子中用户说“来一份水煮鱼”模型调用place_order时items参数应该生成[“水煮鱼 x 1”]。但有时它可能生成[“水煮鱼”]缺少数量。防御性编程在你的工具函数内部要对参数进行校验和清洗。例如检查items列表中的每个字符串是否包含“x”或数量如果没有尝试推断或返回一个错误信息让模型去追问用户。提供清晰的错误反馈当工具执行失败如餐厅不存在、地址无效在返回给模型的content中要清晰地说明错误原因。例如“工具调用失败未找到名为‘不存在的餐厅’的菜单。请确认餐厅名称是否正确。” 模型会根据这个错误信息在下一轮回复中向用户澄清或道歉。参数类型转换模型生成的参数永远是字符串类型因为JSON中值都是字符串。如果你的函数需要整数、浮点数或布尔值务必在调用函数前进行类型转换和验证。4.3 会话长度管理与成本优化多轮对话历史会越来越长而大模型的API收费是基于输入和输出的总token数。历史过长不仅增加成本还可能超过模型的最大上下文长度限制。选择性记忆不要无脑地存储所有历史。可以考虑只保留最近N轮对话或者总结之前的对话内容。例如当一轮订餐完成后下单成功你可以用一条总结性消息如“用户已成功下单订单号XXX”替换掉之前冗长的关于菜品选择和地址确认的对话然后开始新的会话分支。总结与压缩对于非常长的对话可以调用模型自身对之前的对话进行摘要然后将摘要作为新的system或user消息替代旧的长历史。OpenAI的GPT-4 Turbo上下文长度已达128K但对于长期运行的助手管理历史仍是必要功课。分离核心上下文将与当前任务强相关的信息如用户选择的餐厅、已选的菜品提取出来作为独立的变量或数据库记录存储而不是完全依赖对话历史文本。这样在构造Prompt时可以更精准地注入必要信息。4.4 并行工具调用与流式响应较新的模型如GPT-4 Turbo支持并行工具调用。即模型可以在一次回复中同时请求调用多个不相关的工具。这大大提升了效率。在上面的代码中tool_calls就是一个列表我们需要遍历执行所有工具并将所有结果一次性返回给模型。另一个提升体验的功能是流式响应。当模型在“思考”是否要调用工具以及生成文本时你可以通过设置streamTrue来获取流式响应。这对于需要长时间运行的工具调用如查询一个慢速数据库场景很有用你可以先给用户一个“正在处理”的提示。5. 扩展到其他场景与架构思考多轮对话与Function Calling的组合拳其应用远不止订餐助手。智能客服用户报修。模型调用query_knowledge_base工具搜索解决方案如果不行调用create_service_ticket工具创建工单并返回工单号。数据分析助手用户说“帮我分析上个月上海的销售数据”。模型调用execute_sql_query工具获取数据然后调用generate_chart工具生成图表最后用文本总结洞察。智能家居用户说“客厅太热了”。模型调用get_temperature工具确认温度然后调用set_thermostat工具调节空调。在更复杂的架构中你可能会引入“智能体Agent”框架如LangChain、AutoGen等。这些框架提供了更高级的抽象如记忆管理、工具路由、复杂工作流编排。但它们的底层原理依然是本文所讲述的多轮对话状态管理和Function Calling。理解了这个基础你就能更好地使用甚至定制这些框架。最后一点体会在开发这类系统时一定要做好日志记录。详细记录每轮对话的用户输入、模型回复包括原始的tool_calls、工具执行结果和最终输出。这是调试模型诡异行为、优化工具描述、分析用户意图的宝贵资料。很多时候效果不佳不是模型不行而是我们的“说明书”工具描述和系统提示写得不够好。持续地基于真实交互数据迭代优化你的智能体才会越来越聪明、可靠。