如何让智能体可靠调用外部 API
如何让智能体可靠调用外部API?从痛点、原理到工程落地的全流程指南
引言
痛点引入
想象一下你正在开发一个**「AI旅行规划师」智能体**:用户输入「下周从上海去厦门鼓浪屿玩3天,预算4000元,住海景民宿,吃当地特色小吃,避开雨天」。你的智能体立刻拆解任务链:调用天气预报API查厦门下周的雨期→调用OTA(携程/飞猪)API查符合预算和日期的海景民宿→调用地图API规划鼓浪屿岛内步行+电瓶车路线→调用大众点评API排小吃优先级+预约网红店→整合所有信息生成HTML+可交互的旅行清单。
一切看起来完美,结果——
- 调用天气预报API时,因为API服务商的免费额度用完了,直接返回了429 Too Many Requests,智能体却还在傻傻循环重试;
- OTA API返回了一个JSON格式错误,系统提示KeyError: ‘available_rooms’,智能体直接崩溃并给用户回了一句「抱歉,我无法理解您的需求」;
- 大众点评API返回了200条小吃结果,但预算吃紧,智能体随机挑了5个,结果都是人均100+的高端餐厅;
- 最后整合HTML时,民宿地址里的引号没有转义,用户打开网页直接白屏;
- 用户发现行程第二天下午下小雨,要求重排,但智能体居然忘记缓存之前查过的、第三天可用的民宿房源,又重新浪费了3次API额度。
是不是血压一下子上来了?这些API调用可靠性问题,是现在所有接入外部工具链的智能体(Agent)从「Demo级玩具」到「生产级产品」必须跨越的一道坎——而且是一道很难的坎:传统软件开发的API调用可靠性问题(限流、超时、格式错误、幂等性……),加上大语言模型(LLM)本身的「非确定性」「幻觉(Hallucination)」「上下文理解偏差」「结构化能力不稳定」,直接让问题复杂度翻了N倍。
根据2024年3月《AWS Bedrock智能体生产实践白皮书》统计,生产环境中70%以上的智能体故障都与外部API调用相关;OpenAI的Function Calling Beta版发布至今(2024年7月),GitHub上针对其可靠性优化的开源项目已经超过1200个,足见行业对这个问题的重视程度。
解决方案概述
那我们该怎么系统性地解决这个问题呢?本文不会只给你零散的「加一个重试装饰器」「用Pydantic做数据验证」这类技巧——当然这些技巧很重要,但如果没有完整的工程体系和理论支撑,它们只能解决局部问题,治标不治本。
本文将从**「从LLM视角分析API调用的本质」出发,构建一个「6层智能体API可靠调用架构」**:
- API意图理解层:解决LLM会不会选对API、参数会不会填错的问题;
- API输入预处理层:解决幻觉参数、格式不一致、逻辑冲突的问题;
- API调用执行层:解决超时、重试、限流、幂等性等传统工程问题;
- API输出验证层:解决格式错误、数据缺失、逻辑矛盾、安全隐患的问题;
- API上下文与状态管理层:解决数据不一致、上下文丢失、冗余调用的问题;
- API错误恢复与降级层:解决故障时如何优雅应对的问题。
每一层我们都会:
- 深入分析核心痛点和背后的LLM+系统原理;
- 给出可落地的工程方案;
- 附上Python源代码(基于LangChain、OpenAI Function Calling、Pydantic、Tenacity等主流工具);
- 列出最佳实践Tips;
- 用Mermaid架构图/流程图和数学模型辅助理解。
最后,我们会用一个**「简化版AI旅行规划师」**的完整项目案例,把这6层架构串起来,让你看完就能动手实现。
最终效果展示(先看一眼)
通过这套架构,我们的「AI旅行规划师」可以做到:
- 意图理解准确率≥98%:不会把「查机票」误判成「查火车票」;
- 参数填充零幻觉(至少在规则内):不会把「厦门」写成「厦冂」,不会把预算写成「4000万」;
- 传统故障处理率≥99.9%:限流自动退避重试,超时3次后自动降级到备选API;
- 输出验证通过率≥99.5%:返回的JSON全符合Pydantic Schema,数据缺失自动补默认值或触发LLM反思;
- 上下文管理准确率≥99%:不会忘记用户之前的预算、日期等要求,不会重复调用相同参数的API;
- 错误恢复优雅率≥100%(开玩笑):即使所有API都挂了,也会给用户回一句「抱歉,我目前查不到实时信息,但我可以给您一份厦门鼓浪屿的通用攻略」。
准备工作
环境/工具
在开始之前,请确保你的开发环境已经配置好以下工具和依赖库:
| 工具/依赖库 | 版本要求 | 用途 |
|---|---|---|
| Python | ≥3.10 | 主流的LLM和Agent开发语言,语法简洁,生态丰富 |
| pip | ≥23.0 | Python包管理器,用于安装依赖库 |
| OpenAI API Key | - | 用于调用GPT-4o(推荐,结构化能力强)或GPT-3.5-turbo |
| LangChain Core | ≥0.2.0 | LangChain的核心模块,提供工具链、Agent的基础抽象 |
| LangChain OpenAI | ≥0.1.0 | LangChain对OpenAI API的封装,包括Function Calling、ChatOpenAI等 |
| Pydantic | ≥2.0.0 | 数据验证库,用于定义API的输入/输出Schema,解决结构化不稳定的问题 |
| Tenacity | ≥8.2.0 | 重试装饰器库,用于解决超时、限流等临时故障 |
| Python-dotenv | ≥1.0.0 | 环境变量管理库,用于安全存储API Key等敏感信息 |
| Requests | ≥2.31.0 | HTTP请求库,用于调用外部API |
| Redis | ≥7.0.0 | 缓存/分布式锁库,用于解决上下文丢失、冗余调用、幂等性的问题(可选) |
| Postman | - | API调试工具,用于预先验证外部API的格式和可用性 |
环境安装步骤
- 创建Python虚拟环境(强烈推荐,避免依赖冲突):
# Windows python -m venv agent_api_env agent_api_env\Scripts\activate # macOS/Linux python3 -m venv agent_api_env source agent_api_env/bin/activate - 安装依赖库:
pip install langchain-core langchain-openai pydantic tenacity python-dotenv requests redis - 配置环境变量:
在项目根目录下创建一个.env文件,内容如下:# OpenAI API配置 OPENAI_API_KEY=你的OpenAI_API_Key OPENAI_MODEL_NAME=gpt-4o-mini # 开发阶段用gpt-4o-mini省钱,生产环境用gpt-4o OPENAI_TEMPERATURE=0.0 # 结构化输出和意图理解阶段用0.0,避免非确定性 # 模拟外部API配置(我们后面会自己写几个模拟API,方便演示) MOCK_WEATHER_API_URL=http://localhost:8000/weather MOCK_OTA_API_URL=http://localhost:8000/ota MOCK_DIANPING_API_URL=http://localhost:8000/dianping # Redis配置(可选) REDIS_HOST=localhost REDIS_PORT=6379 REDIS_DB=0 - 验证OpenAI API是否可用:
新建一个test_openai.py文件,内容如下:
运行后如果能看到类似「我是OpenAI开发的GPT-4o-mini,一个人工智能助手,擅长各种语言任务和结构化输出」的回复,说明环境配置成功。from dotenv import load_dotenv from langchain_openai import ChatOpenAI import os # 加载环境变量 load_dotenv() # 初始化ChatOpenAI llm = ChatOpenAI( model=os.getenv("OPENAI_MODEL_NAME"), temperature=float(os.getenv("OPENAI_TEMPERATURE")), api_key=os.getenv("OPENAI_API_KEY") ) # 测试调用 response = llm.invoke("你好,请用一句话介绍自己") print(response.content)
基础知识
本文假设你已经具备以下基础知识,如果没有,可以先去补充学习:
- Python编程基础:变量、函数、类、装饰器、异常处理、JSON操作等;
- HTTP协议基础:GET/POST请求、状态码(200/400/401/403/404/429/500/503)、请求头/响应头、JSON数据格式等;
- 大语言模型(LLM)基础:Prompt Engineering(提示词工程)、Function Calling(工具调用)、Hallucination(幻觉)、Temperature(温度参数)等;
- 基础软件工程知识:幂等性、缓存、分布式锁、降级、熔断等。
以下是一些推荐的学习资源链接:
- Python编程基础:廖雪峰的Python教程
- HTTP协议基础:MDN Web Docs - HTTP
- LLM基础与Prompt Engineering:OpenAI官方Prompt Engineering指南、吴恩达+OpenAI的Prompt Engineering课程
- Function Calling基础:OpenAI官方Function Calling文档
- 基础软件工程知识:Martin Fowler的《企业应用架构模式》(有点厚,但经典)、Netflix的《Chaos Engineering》
核心概念:从LLM视角分析API调用的本质
在开始讲具体的架构和代码之前,我们必须先搞清楚一个问题:LLM到底是怎么调用外部API的?它和传统软件的API调用有什么本质区别?
很多人对LLM调用API的理解停留在「LangChain封装了Function Calling,我只要把工具定义好,剩下的交给LLM就行」——这种理解是完全错误的,也是导致很多Demo级Agent在生产环境中崩溃的根本原因。
传统软件的API调用:确定性的、指令驱动的
先回忆一下传统软件的API调用流程:
核心特点:
- 完全确定性:所有逻辑都是开发者写死的——选哪个API、填什么参数、怎么检查合法性、怎么解析响应、出了问题怎么处理,全都是确定的;
- 指令驱动:开发者告诉软件「当用户输入X时,你就执行Y操作」,软件只是执行指令的机器;
- 参数来源明确:参数要么来自用户输入的结构化表单(比如下拉框选日期,数字输入框填预算,基本不会有格式错误),要么来自数据库的固定值;
- 响应格式固定:API的响应格式是开发者和API服务商提前约定好的JSON Schema,解析逻辑也是写死的,几乎不会出问题。
LLM驱动的API调用:非确定性的、意图驱动的
再看LLM驱动的API调用流程(以OpenAI Function Calling为例):
核心特点:
- 非确定性:所有核心逻辑(选工具、填参数、整合响应)都是LLM通过概率模型生成的——即使是相同的用户输入,LLM也可能会生成不同的工具选择或参数;
- 意图驱动:开发者不再告诉LLM「当用户输入X时,你就执行Y操作」,而是告诉LLM「这里有几个工具,每个工具的功能是什么,输入输出是什么,你自己根据用户的意图选择工具」;
- 参数来源不确定:参数完全来自LLM对用户自然语言输入的理解——可能会有幻觉(比如把「厦门」写成「厦冂」)、格式错误(比如把「2024-08-10」写成「2024/8/10」或「明天」)、逻辑冲突(比如预算4000元却选了人均2000元的民宿);
- 响应理解不确定:LLM对API响应的理解也是非确定性的——如果响应格式不对或数据缺失,LLM可能会编造数据,也可能会直接崩溃;
- 上下文依赖强:LLM的工具选择和参数填充依赖于之前的对话上下文——如果上下文丢失,LLM可能会忘记用户之前的预算、日期等要求。
概念结构与核心要素组成
既然LLM驱动的API调用和传统软件的有本质区别,那我们就需要重新定义一下它的核心概念和要素组成:
核心概念
- Agent(智能体):本文中特指接入外部API工具链的LLM应用,它可以根据用户的自然语言意图,自主选择工具、生成参数、执行调用、整合响应、回复用户;
- Tool(工具):Agent可以调用的外部能力的封装,包括API工具(比如天气预报API、OTA API)、代码执行工具(比如Python REPL)、数据库查询工具等——本文只讨论API工具;
- Tool Schema(工具定义):用来描述工具功能、输入参数、输出格式的结构化数据,通常是JSON Schema或Pydantic Model,是LLM理解工具的核心;
- Function Calling(工具调用):LLM的一种能力,当用户的意图需要使用外部工具时,LLM会生成一个JSON格式的「工具调用请求」,而不是直接回复自然语言;
- Tool Response(工具响应):API返回的结果,通常是JSON格式,需要Agent验证后再传给LLM;
- Hallucination(幻觉):LLM生成的不符合事实、不符合逻辑、不符合工具要求的数据或内容;
- Idempotency(幂等性):指对同一个API调用请求,执行一次和执行多次的结果是完全相同的——这对于解决重试带来的副作用问题非常重要;
- Degradation(降级):指当某个API工具不可用时,Agent自动切换到一个功能类似但可靠性/精度稍低的备选工具,或者返回一个预定义的默认内容;
- Circuit Breaker(熔断器):指当某个API工具的错误率超过阈值时,Agent暂时停止调用该工具,直接返回降级内容,避免浪费资源和影响用户体验;
- Context Window(上下文窗口):指LLM可以处理的最大Token数,超过这个数的对话内容会被LLM遗忘——这是导致上下文丢失的主要原因。
核心要素组成(6层架构的雏形)
基于以上核心概念,我们可以把LLM驱动的API可靠调用拆解成6个核心要素,对应后面要讲的6层架构:
- 准确的意图理解与工具选择:对应第1层「API意图理解层」;
- 合法的、逻辑一致的参数生成:对应第2层「API输入预处理层」;
- 稳定的API调用执行:对应第3层「API调用执行层」;
- 可信的、格式正确的响应验证:对应第4层「API输出验证层」;
- 完整的上下文与状态管理:对应第5层「API上下文与状态管理层」;
- 优雅的错误恢复与降级:对应第6层「API错误恢复与降级层」。
概念之间的关系:ER实体关系图与交互流程图
ER实体关系图
交互流程图(完整的6层架构交互)
第1层:API意图理解层——解决会不会选对工具、填错参数的问题
API意图理解层是整个架构的第一道门槛——如果LLM选不对工具,后面的所有工作都是白搭;如果LLM生成的初始参数错误率太高,后面的预处理层也会不堪重负。
核心痛点
- 工具选择错误:比如用户说「我要订机票」,LLM却选了「订火车票」的工具;或者用户说「我要查上海到北京的航班」,LLM同时选了「查航班」和「查火车票」的工具(重复调用);
- 初始参数填充错误率高:比如工具要求参数是「date: YYYY-MM-DD」,LLM却生成了「date: 明天」或「date: 2024/8/10」;比如工具要求参数是「budget: float(元)」,LLM却生成了「budget: 4000万」或「budget: 四千元」;
- 上下文理解偏差:比如用户之前说「预算4000元,不含机票」,LLM在后面查民宿时却把机票钱也算进去了;
- 多余工具调用:比如用户只是问「厦门鼓浪屿好玩吗?」,LLM却调用了「查天气预报」和「查民宿」的工具(虽然用户没有明确要求)。
背后的原理
为什么LLM会出现这些问题呢?我们从Prompt Engineering、Tool Schema设计、LLM本身的能力三个角度来分析:
1. Prompt Engineering角度
很多开发者在写Agent的System Prompt(系统提示词)时,写得太笼统了,比如:
「你是一个AI旅行规划师,这里有几个工具,你可以根据用户的需求选择工具。」
这样的Prompt没有给LLM足够的约束和示例,LLM只能靠概率模型来选择工具和填充参数——自然会出错。
根据OpenAI官方Prompt Engineering指南,好的System Prompt应该包含以下几个部分:
- 角色定义:明确LLM的身份和职责;
- 约束条件:明确LLM不能做什么,比如「不要编造数据」「不要调用多余的工具」;
- 工具使用说明:明确LLM什么时候应该使用工具,什么时候应该直接回复;
- 示例对话:给LLM提供几个「好的」和「坏的」示例对话,让LLM学习如何正确地选择工具和填充参数;
- 输出格式要求:如果需要结构化输出(比如工具调用后的整合回复),明确输出格式。
2. Tool Schema设计角度
Tool Schema是LLM理解工具的唯一途径——如果Schema写得不好,LLM根本不知道该怎么用这个工具。
很多开发者在写Tool Schema时,犯了以下几个错误:
- 工具功能描述太笼统:比如把「查天气」的工具功能描述成「查询天气信息」,而不是「查询指定城市在指定日期范围内的天气预报,包括温度、降水概率、风力等信息」;
- 参数描述太模糊:比如把「date」参数的描述写成「日期」,而不是「需要查询的日期,格式必须是YYYY-MM-DD,比如2024-08-10」;
- 没有明确必填参数和可选参数:LLM可能会漏掉必填参数,或者给可选参数填一些没用的值;
- 没有给参数设置枚举值(Enum):比如「city」参数,LLM可能会生成「厦冂」「厦门岛」「厦门市思明区」等不同的写法,如果设置了枚举值(比如[“上海”, “北京”, “厦门”, “广州”]),LLM就不会出错了;
- 没有给参数设置最小值/最大值:比如「budget」参数,LLM可能会生成「-100」或「1000000000」的值,如果设置了最小值0和最大值100000,LLM就会收敛很多。
3. LLM本身的能力角度
即使Prompt和Schema都写得很好,LLM本身的能力也有限制:
- 非确定性:即使是相同的用户输入和相同的Prompt,LLM也可能会生成不同的工具选择或参数——解决办法是把Temperature设置成0.0;
- Context Window限制:如果对话历史太长,超过了LLM的Context Window,LLM就会忘记之前的约束条件和用户要求——解决办法是对对话历史进行「摘要」或「裁剪」,只保留最近的、最重要的内容;
- 结构化能力不稳定:虽然GPT-4o和GPT-3.5-turbo的结构化能力已经很强了,但在处理复杂的Schema(比如嵌套的JSON Schema)时,还是可能会出错——解决办法是用「结构化输出强制模式」(比如OpenAI的JSON Mode或Structured Outputs),或者用Pydantic做验证。
工程方案
基于以上原理分析,我们可以提出以下几个可落地的工程方案:
方案1:写一份「高质量的System Prompt」
这里我们给「AI旅行规划师」写一份高质量的System Prompt,包含所有必要的部分:
SYSTEM_PROMPT = """
你是一个专业的、可靠的AI旅行规划师,你的名字叫「小驴」。
### 1. 角色定义与职责
你的主要职责是根据用户的自然语言需求,规划合理的旅行方案,包括但不限于:
- 查询指定城市在指定日期范围内的天气预报;
- 查询符合预算、日期、位置等要求的酒店/民宿;
- 查询当地特色小吃和景点;
- 规划旅行路线。
### 2. 严格的约束条件
- **永远不要编造数据**:如果你不知道某个信息,或者工具返回的信息不完整,请明确告诉用户,不要自己瞎编;
- **不要调用多余的工具**:只有当用户的需求明确需要外部实时信息或你无法直接回答时,才调用工具——比如用户只是问「厦门鼓浪屿好玩吗?」,你可以直接回答,不需要调用任何工具;
- **严格遵守Tool Schema的要求**:工具的参数格式必须和Schema完全一致,必填参数不能少,可选参数如果不需要可以不填;
- **优先考虑用户之前的对话上下文**:不要忘记用户之前提到的预算、日期、旅行目的地等要求。
### 3. 工具使用说明
你有以下几个工具可以使用:
1. **get_weather**:查询指定城市在指定日期范围内的天气预报;
2. **search_accommodation**:查询符合要求的酒店/民宿;
3. **search_food**:查询当地特色小吃;
4. **search_attractions**:查询当地景点。
什么时候应该使用工具?
- 当用户的需求涉及到「实时信息」(比如天气预报、酒店房间 availability)时;
- 当用户的需求涉及到「外部数据」(比如当地特色小吃、景点)时;
- 当你无法直接回答用户的问题时。
什么时候应该直接回复?
- 当用户的需求只是「闲聊」时;
- 当用户的需求只是「询问常识性问题」时;
- 当用户的需求不需要外部实时信息或数据时。
### 4. 示例对话
#### 示例1:正确的工具选择和参数填充
**用户**:下周从上海去厦门鼓浪屿玩3天,预算4000元,住海景民宿,吃当地特色小吃,避开雨天。
**小驴(工具调用请求)**:
```json
{
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": {
"city": "厦门",
"start_date": "2024-08-12",
"end_date": "2024-08-14"
}
}
}
]
}
(假设今天是2024-08-10)
示例2:错误的工具选择和参数填充(不要这样做)
用户:下周从上海去厦门鼓浪屿玩3天,预算4000元,住海景民宿,吃当地特色小吃,避开雨天。
小驴(工具调用请求):
{
"tool_calls": [
{
"id": "call_456",
"type": "function",
"function": {
"name": "search_flights",
"arguments": {
"departure": "上海",
"arrival": "厦冂",
"date": "明天",
"budget": "四千元"
}
}
},
{
"id": "call_789",
"type": "function",
"function": {
"name": "search_attractions",
"arguments": {
"city": "北京"
}
}
}
]
}
错误点:
- 没有这个工具「search_flights」;
- 城市写成了「厦冂」(幻觉);
- 日期写成了「明天」(不符合Schema要求的YYYY-MM-DD);
- 预算写成了「四千元」(不符合Schema要求的float);
- 调用了多余的工具「search_attractions」,而且城市写成了「北京」(上下文理解偏差)。
5. 输出格式要求
如果你需要调用工具,请严格按照JSON格式输出,不要添加任何额外的自然语言内容;
如果你不需要调用工具,请直接用自然语言回复用户,语气要友好、专业。
“”"
#### 方案2:用Pydantic v2定义「高质量的Tool Schema」
Pydantic v2是一个非常强大的数据验证库,它可以:
1. 自动将Python类转换成JSON Schema(OpenAI Function Calling需要的格式);
2. 自动验证参数的类型、格式、取值范围等;
3. 支持嵌套的JSON Schema;
4. 支持自定义验证逻辑。
这里我们用Pydantic v2定义「AI旅行规划师」的4个工具的Schema:
首先,我们需要导入一些必要的模块:
```python
from typing import List, Optional
from pydantic import BaseModel, Field, field_validator, ValidationError
from datetime import date, timedelta
然后,我们定义几个通用的Schema:
# 通用的位置Schema
class Location(BaseModel):
city: str = Field(..., description="城市名称,必须是中国大陆的主要城市,比如上海、北京、厦门", enum=["上海", "北京", "厦门", "广州", "深圳", "杭州", "成都", "西安"])
district: Optional[str] = Field(None, description="区县名称,比如思明区、浦东新区")
# 通用的日期范围Schema
class DateRange(BaseModel):
start_date: date = Field(..., description="开始日期,格式必须是YYYY-MM-DD,比如2024-08-10")
end_date: date = Field(..., description="结束日期,格式必须是YYYY-MM-DD,比如2024-08-14")
# 自定义验证逻辑:结束日期必须晚于开始日期,且日期范围不能超过7天
@field_validator('end_date')
@classmethod
def end_date_must_be_after_start_date(cls, v: date, info) -> date:
if 'start_date' in info.data and v <= info.data['start_date']:
raise ValueError('结束日期必须晚于开始日期')
if 'start_date' in info.data and (v - info.data['start_date']).days > 7:
raise ValueError('日期范围不能超过7天')
return v
接下来,我们定义4个工具的输入Schema:
# 工具1:get_weather的输入Schema
class GetWeatherInput(BaseModel):
location: Location = Field(..., description="需要查询的位置")
date_range: DateRange = Field(..., description="需要查询的日期范围")
# 工具2:search_accommodation的输入Schema
class SearchAccommodationInput(BaseModel):
location: Location = Field(..., description="需要查询的位置")
date_range: DateRange = Field(..., description="入住和退房日期范围")
budget_per_night: float = Field(..., description="每晚的预算,单位是元,必须大于0且小于等于10000", ge=0.0, le=10000.0)
room_type: Optional[str] = Field(None, description="房间类型,比如海景房、标准间、大床房", enum=["海景房", "标准间", "大床房", "套房"])
guest_count: Optional[int] = Field(2, description="入住人数,必须大于0且小于等于10", ge=1, le=10)
# 工具3:search_food的输入Schema
class SearchFoodInput(BaseModel):
location: Location = Field(..., description="需要查询的位置")
budget_per_meal: Optional[float] = Field(None, description="每餐的预算,单位是元,必须大于0且小于等于1000", ge=0.0, le=1000.0)
food_type: Optional[str] = Field(None, description="美食类型,比如当地特色小吃、海鲜、火锅", enum=["当地特色小吃", "海鲜", "火锅", "川菜", "粤菜"])
top_k: Optional[int] = Field(5, description="返回的结果数量,必须大于0且小于等于20", ge=1, le=20)
# 工具4:search_attractions的输入Schema
class SearchAttractionsInput(BaseModel):
location: Location = Field(..., description="需要查询的位置")
attraction_type: Optional[str] = Field(None, description="景点类型,比如自然风光、历史文化、主题公园", enum=["自然风光", "历史文化", "主题公园", "亲子游"])
top_k: Optional[int] = Field(5, description="返回的结果数量,必须大于0且小于等于20", ge=1, le=20)
然后,我们定义4个工具的输出Schema(虽然OpenAI Function Calling不强制要求输出Schema,但定义它可以帮助我们后面做验证):
# 工具1:get_weather的输出Schema
class DailyWeather(BaseModel):
date: date = Field(..., description="日期,格式YYYY-MM-DD")
max_temp: int = Field(..., description="最高温度,单位摄氏度", ge=-50, le=50)
min_temp: int = Field(..., description="最低温度,单位摄氏度", ge=-50, le=50)
weather_condition: str = Field(..., description="天气状况,比如晴、多云、小雨", enum=["晴", "多云", "阴", "小雨", "中雨", "大雨", "雷阵雨"])
precipitation_probability: int = Field(..., description="降水概率,百分比,必须在0到100之间", ge=0, le=100)
wind_speed: int = Field(..., description="风力,单位公里/小时,必须在0到200之间", ge=0, le=200)
class GetWeatherOutput(BaseModel):
location: Location = Field(..., description="查询的位置")
daily_forecasts: List[DailyWeather] = Field(..., description="每日天气预报列表")
# 工具2:search_accommodation的输出Schema
class Accommodation(BaseModel):
id: str = Field(..., description="酒店/民宿的唯一ID")
name: str = Field(..., description="酒店/民宿的名称")
address: str = Field(..., description="酒店/民宿的地址")
price_per_night: float = Field(..., description="每晚的价格,单位元")
room_type: str = Field(..., description="房间类型")
guest_count: int = Field(..., description="可入住人数")
rating: float = Field(..., description="评分,必须在0到5之间", ge=0.0, le=5.0)
review_count: int = Field(..., description="评论数量,必须大于等于0", ge=0)
is_available: bool = Field(..., description="是否有房")
class SearchAccommodationOutput(BaseModel):
location: Location = Field(..., description="查询的位置")
date_range: DateRange = Field(..., description="入住和退房日期范围")
accommodations: List[Accommodation] = Field(..., description="符合要求的酒店/民宿列表")
# 工具3和4的输出Schema这里就省略了,类似工具1和2
最后,我们需要将这些Pydantic Model转换成OpenAI Function Calling需要的格式——LangChain已经给我们提供了一个工具类StructuredTool,可以自动完成这个转换:
from langchain_core.tools import StructuredTool
import requests
from dotenv import load_dotenv
import os
# 加载环境变量
load_dotenv()
# 先定义几个模拟的工具函数(后面我们会自己写模拟API,这里先写个空的)
def mock_get_weather(location: Location, date_range: DateRange) -> GetWeatherOutput:
# 这里先返回一个空的GetWeatherOutput,后面我们会替换成调用真实的模拟API
return GetWeatherOutput(location=location, daily_forecasts=[])
def mock_search_accommodation(location: Location, date_range: DateRange, budget_per_night: float, room_type: Optional[str] = None, guest_count: int = 2) -> SearchAccommodationOutput:
return SearchAccommodationOutput(location=location, date_range=date_range, accommodations=[])
def mock_search_food(location: Location, budget_per_meal: Optional[float] = None, food_type: Optional[str] = None, top_k: int = 5) -> dict:
return {}
def mock_search_attractions(location: Location, attraction_type: Optional[str] = None, top_k: int = 5) -> dict:
return {}
# 将Pydantic Model转换成StructuredTool
get_weather_tool = StructuredTool.from_function(
func=mock_get_weather,
name="get_weather",
description="查询指定城市在指定日期范围内的天气预报,包括温度、降水概率、风力等信息",
args_schema=GetWeatherInput,
return_direct=False # 工具返回的结果需要传给LLM整合,而不是直接返回给用户
)
search_accommodation_tool = StructuredTool.from_function(
func=mock_search_accommodation,
name="search_accommodation",
description="查询符合预算、日期、位置、房间类型等要求的酒店/民宿",
args_schema=SearchAccommodationInput,
return_direct=False
)
search_food_tool = StructuredTool.from_function(
func=mock_search_food,
name="search_food",
description="查询当地特色小吃、海鲜等美食",
args_schema=SearchFoodInput,
return_direct=False
)
search_attractions_tool = StructuredTool.from_function(
func=mock_search_attractions,
name="search_attractions",
description="查询当地自然风光、历史文化等景点",
args_schema=SearchAttractionsInput,
return_direct=False
)
# 把所有工具放在一个列表里
tools = [get_weather_tool, search_accommodation_tool, search_food_tool, search_attractions_tool]
方案3:使用「结构化输出强制模式」和「Temperature=0.0」
虽然Prompt和Schema都写得很好了,但LLM还是可能会生成不符合格式的工具调用请求——这时候我们就需要使用「结构化输出强制模式」。
OpenAI提供了两种结构化输出强制模式:
- JSON Mode:强制LLM返回JSON格式的内容,但不强制JSON的结构(需要自己验证);
- Structured Outputs:强制LLM返回符合指定JSON Schema的内容(2024年7月刚发布的新功能,只有GPT-4o和GPT-4o-mini支持,非常强大)。
这里我们推荐使用Structured Outputs,因为它可以从根源上解决LLM生成不符合格式的工具调用请求的问题。
另外,我们需要把Temperature设置成0.0,这样可以最大限度地减少LLM的非确定性——相同的用户输入和Prompt,LLM会生成完全相同的工具选择和参数。
我们来修改一下之前初始化的ChatOpenAI:
from langchain_openai import ChatOpenAI
# 初始化支持Structured Outputs的ChatOpenAI
llm = ChatOpenAI(
model=os.getenv("OPENAI_MODEL_NAME"),
temperature=float(os.getenv("OPENAI_TEMPERATURE")),
api_key=os.getenv("OPENAI_API_KEY"),
# 启用Structured Outputs(只有GPT-4o和GPT-4o-mini支持)
# 注意:LangChain目前对Structured Outputs的封装还不是很完善,我们后面会用OpenAI的原生API来演示
)
方案4:使用「对话历史摘要」或「对话历史裁剪」避免Context Window限制
随着对话的进行,对话历史会越来越长,超过LLM的Context Window——这时候LLM就会忘记之前的约束条件和用户要求。
解决这个问题的方法有两个:
- 对话历史摘要:定期(比如每10轮对话)用LLM对之前的对话历史进行摘要,只保留最重要的内容(比如用户的预算、日期、旅行目的地、之前的工具调用结果等);
- 对话历史裁剪:只保留最近的N轮对话(比如最近的20轮),或者只保留最近的X个Token(比如最近的128000个Token,GPT-4o的Context Window是128000个Token)。
这里我们先讲一下对话历史裁剪的实现方法——LangChain已经给我们提供了一个工具类ConversationBufferWindowMemory,可以自动裁剪对话历史,只保留最近的K轮对话:
from langchain_core.memory import ConversationBufferWindowMemory
# 初始化ConversationBufferWindowMemory,只保留最近的5轮对话
memory = ConversationBufferWindowMemory(
k=5, # 保留最近的5轮对话
return_messages=True, # 返回Message对象,而不是字符串
memory_key="chat_history", # 对话历史的key,后面会用到
output_key="output" # 输出的key,后面会用到
)
对话历史摘要的实现方法稍微复杂一点,我们后面会在「第5层:API上下文与状态管理层」详细讲解。
最佳实践Tips
- 角色定义要具体:不要只说「你是一个
更多推荐
所有评论(0)