1. 项目概述:当大语言模型不支持原生函数调用时怎么办?
在构建基于大语言模型(LLM)的应用时,函数调用(Function Calling)是一个非常实用的功能。它允许模型在对话过程中主动触发外部工具或API,比如查询天气、执行计算或访问数据库。像GPT-4这样的先进模型原生支持这一功能,提供了bind_tools()和tool_calls等便捷接口。然而,当我们使用一些国产模型(如Moonshot)或开源模型(如Llama、Qwen)时,经常会遇到一个尴尬的问题:这些模型并不支持原生的函数调用功能。
这种情况就像你有一把瑞士军刀,但关键的刀片被锁住了无法使用。面对这种限制,开发者通常有两种选择:要么放弃使用这些模型,要么寻找替代方案。本文将详细介绍如何通过Prompt Engineering(提示工程)的方式,让不支持原生函数调用的模型也能实现类似的功能。
核心思路其实很简单:既然模型不能直接返回结构化的函数调用对象,那我们就让模型返回一个结构化的文本(如JSON),然后我们手动解析这个文本并执行对应的函数。这种方法虽然不如原生支持那么优雅,但在很多场景下完全够用,而且具有更好的兼容性和灵活性。
2. 核心原理与设计思路
2.1 为什么需要函数调用功能?
在传统的对话系统中,模型只能生成文本响应。但在实际应用中,我们经常需要模型能够执行一些具体操作,比如:
- 查询实时信息(天气、股票、新闻等)
- 进行专业计算或数据分析
- 操作外部系统或数据库
- 执行特定的业务流程
函数调用功能让模型从"只能说说"变成了"既能说又能做",大大扩展了其应用场景。这也是为什么像GPT-4这样的先进模型都内置了这一功能。
2.2 原生函数调用与Prompt Engineering方式的对比
原生函数调用(如GPT-4)的工作流程是这样的:
- 用户提出问题(如"北京天气怎么样?")
- 模型识别需要调用哪个函数(如weather_query)
- 模型返回结构化的函数调用对象(包含函数名和参数)
- 系统执行对应的函数
- 将函数结果返回给模型
- 模型生成最终回答
而我们的Prompt Engineering方案则是:
- 用户提出问题
- 通过精心设计的Prompt,让模型返回一个JSON格式的文本响应
- 手动解析这个JSON,提取函数名和参数
- 执行对应的函数
- 将结果整合到最终输出中
两者的核心区别在于:原生方式是模型直接返回结构化对象,而我们的方案是让模型返回结构化文本,然后手动解析。虽然多了一步解析过程,但最终效果几乎一样。
2.3 技术实现的关键点
要实现这个方案,有几个关键的技术点需要注意:
- Prompt设计:必须明确告诉模型我们期望的输出格式,并提供清晰的工具描述
- JSON解析:需要可靠地解析模型返回的JSON,处理可能的格式错误
- 工具映射:建立工具名称与实际工具函数的映射关系
- 错误处理:当模型返回不符合预期的内容时,要有备选方案
3. 完整实现方案
3.1 环境准备与工具定义
首先,我们需要准备Python环境并安装必要的库:
bash复制pip install langchain langchain-openai python-dotenv requests pydantic
然后,我们定义一个简单的天气查询工具作为示例:
python复制from pydantic import Field, BaseModel
from langchain_core.tools import BaseTool
class WeatherArgsSchema(BaseModel):
city: str = Field(description="需要查询天气的城市,例如:北京")
class WeatherTool(BaseTool):
"""查询指定城市的天气预报"""
name: str = "weather_query"
description: str = "当你需要查询天气信息时使用此工具"
args_schema: Type[BaseModel] = WeatherArgsSchema
def _run(self, city: str) -> str:
# 这里应该是实际的天气API调用
# 为了示例,我们返回模拟数据
return f"{city}今天晴天,温度 25°C"
这个工具定义包含了几个关键部分:
- 参数模式(WeatherArgsSchema):定义了工具需要的参数及其描述
- 工具名称和描述:这些会被用在Prompt中,帮助模型理解何时使用这个工具
- 实际执行函数(_run):这里我们简化了实现,实际应用中应该调用真实的天气API
3.2 构建Prompt模板
Prompt的设计是整个方案的核心。我们需要明确告诉模型:
- 有哪些工具可用
- 每个工具的用途和参数
- 期望的输出格式
以下是Prompt的构建代码:
python复制from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import render_text_description_and_args
# 准备工具列表
tools = [WeatherTool()]
rendered_tools = render_text_description_and_args(tools)
system_prompt = f"""
你是一个聊天机器人,可以访问以下工具。
以下是每个工具的名称和描述:
{rendered_tools}
根据用户输入,返回要使用的工具的名称和输入。
将您的响应作为具有`name`和`arguments`键的JSON块返回。
`arguments`应该是一个字典,其中键对应于参数名称,值对应于请求的值。
"""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{query}")
])
这个Prompt有几个关键特点:
- 使用系统消息明确说明工具的可用性和格式要求
- 使用
render_text_description_and_args自动生成工具描述 - 明确要求模型返回JSON格式的响应
3.3 创建处理链
接下来,我们需要构建一个处理链,将Prompt、模型调用、JSON解析和工具执行串联起来:
python复制from langchain_core.output_parsers import JsonOutputParser
from langchain_core.runnables import RunnableConfig, RunnablePassthrough
from langchain_openai import ChatOpenAI
from typing import TypedDict, Dict, Any, Optional
# 定义工具调用请求的类型
class ToolCallRequest(TypedDict):
name: str
arguments: Dict[str, Any]
# 创建工具字典
tool_dict = {tool.name: tool for tool in tools}
# 定义工具执行函数
def invoke_tool(
tool_call_request: ToolCallRequest,
config: Optional[RunnableConfig] = None
) -> str:
"""执行工具调用"""
name = tool_call_request["name"]
requested_tool = tool_dict.get(name)
if not requested_tool:
raise ValueError(f"未知工具: {name}")
return requested_tool.invoke(tool_call_request.get("arguments", {}), config=config)
# 使用不支持函数调用的模型
llm = ChatOpenAI(model="moonshot-v1-8k", temperature=0)
# 构建完整处理链
chain = (
prompt
| llm
| JsonOutputParser() # 解析模型输出的JSON
| RunnablePassthrough.assign(output=invoke_tool) # 执行工具并将结果赋值给output
)
这个处理链的工作流程是:
- 接收用户查询
- 应用Prompt模板
- 调用语言模型
- 解析模型返回的JSON
- 执行对应的工具
- 将工具结果添加到输出中
3.4 调用示例
现在我们可以测试这个解决方案了:
python复制result = chain.invoke({"query": "北京今天天气怎么样?"})
print(result)
预期输出类似于:
json复制{
"name": "weather_query",
"arguments": {"city": "北京"},
"output": "北京今天晴天,温度 25°C"
}
4. 高级技巧与优化建议
4.1 处理模型返回的不规范JSON
有时候,模型可能返回不完全符合JSON格式的内容,比如在JSON块前后添加了额外的文本。我们可以通过以下方式���强JSON解析的鲁棒性:
python复制import json
import re
def robust_json_parser(text: str) -> dict:
"""更健壮的JSON解析器"""
# 尝试提取第一个完整的JSON块
match = re.search(r'\{.*\}', text, re.DOTALL)
if not match:
raise ValueError("未找到有效的JSON内容")
try:
return json.loads(match.group())
except json.JSONDecodeError as e:
raise ValueError(f"JSON解析失败: {e}")
然后在处理链中使用这个解析器替代默认的JsonOutputParser。
4.2 支持多工具调用
有些场景可能需要同时调用多个工具。我们可以修改Prompt和解析逻辑来支持这一点:
python复制system_prompt_multitool = f"""
你是一个聊天机器人,可以访问以下工具:
{rendered_tools}
根据用户输入,可能需要使用多个工具。
将您的响应作为JSON数组返回,每个元素是一个工具调用对象,
包含`name`和`arguments`键。
示例:
[
{{"name": "tool1", "arguments": {{"param1": "value1"}}}},
{{"name": "tool2", "arguments": {{"param2": "value2"}}}}
]
"""
# 修改invoke_tool函数处理多个工具调用
def invoke_multiple_tools(tool_requests: List[ToolCallRequest]) -> Dict[str, str]:
results = {}
for req in tool_requests:
results[req["name"]] = invoke_tool(req)
return results
4.3 性能优化技巧
- 缓存工具描述:如果工具列表不变,可以缓存rendered_tools,避免每次调用都重新生成
- 批量处理:对于多个相关查询,可以设计支持批量处理的Prompt
- 并行执行:当调用多个独立工具时,可以使用线程池并行执行
4.4 错误处理与回退机制
在实际应用中,我们需要考虑各种错误情况:
- 模型返回无效JSON:提供清晰的错误信息,并可能要求模型重试
- 工具执行失败:捕获工具异常,提供有意义的错误响应
- 模型选择错误工具:可以设计验证步骤,确认工具选择是否合理
一个简单的错误处理改进:
python复制from typing import List, Union
def safe_invoke_tool(
tool_call_request: Union[ToolCallRequest, List[ToolCallRequest]],
config: Optional[RunnableConfig] = None
) -> Union[str, Dict[str, str]]:
try:
if isinstance(tool_call_request, list):
return invoke_multiple_tools(tool_call_request)
return invoke_tool(tool_call_request, config)
except Exception as e:
return f"工具执行失败: {str(e)}"
5. 实际应用中的经验分享
在实际项目中应用这种技术方案时,我积累了一些宝贵的经验:
5.1 Prompt设计的艺术
- 明确示例很重要:在Prompt中提供1-2个清晰的输入输出示例,能显著提高模型返回正确格式的概率
- 工具描述要具体:工具的描述和参数说明越详细,模型选择正确的可能性越高
- 限制输出格式:明确要求模型"只返回JSON,不要添加任何额外解释"
5.2 模型选择的影响
- 模型对JSON的理解能力:不同模型处理结构化输出的能力差异很大,需要进行测试
- 温度参数设置:对于工具调用场景,通常应该设置temperature=0以获得最确定性的结果
- 上下文长度:确保模型的上下文窗口足够容纳所有工具描述
5.3 调试技巧
- 记录完整交互:保存模型输入的Prompt和原始输出,便于分析问题
- 逐步验证:先单独测试Prompt效果,再测试完整流程
- 边界测试:尝试各种边缘情况,如空输入、模糊查询等
5.4 性能考量
- 延迟分析:这种方案相比原生函数调用会增加一些延迟,需要评估是否可接受
- Token使用:详细的工具描述会消耗更多Token,可能增加成本
- 缓存策略:对于频繁使用的工具结果,考虑添加缓存层
6. 扩展应用场景
这种技术方案不仅适用于工具调用,还可以应用于其他需要结构化输出的场景:
6.1 数据提取
从非结构化文本中提取结构化信息,如:
- 从邮件中提取订单详情
- 从报告中提取关键指标
- 从对话中识别用户意图和实体
6.2 多步骤任务分解
将复杂任务分解为多个步骤,每个步骤对应一个工具或操作:
- 分析用户需求
- 设计解决方案步骤
- 按顺序执行各步骤
- 整合最终结果
6.3 决策支持系统
构建需要多因素分析的决策系统:
- 收集相关信息(调用多个查询工具)
- 分析各因素权重
- 生成建议方案
7. 替代方案比较
除了本文介绍的方法外,还有其他几种解决函数调用限制的方案:
7.1 微调模型
对开源模型进行微调,使其理解特定的函数调用格式。这种方案的优势是:
- 可以获得更稳定的输出
- 减少对Prompt工程的依赖
但缺点也很明显: - 需要训练数据和资源
- 灵活性较差,新增工具需要重新微调
7.2 使用中间层模型
架构设计为:
- 主模型处理用户交互
- 专用的小模型处理工具调用决策
这种方案的优点是:
- 可以针对工具调用专门优化
- 减少主模型的负担
缺点是: - 系统复杂度增加
- 需要维护多个模型
7.3 规则引擎+LLM混合
结合传统规则引擎和LLM:
- 规则引擎先进行意图识别
- LLM处理复杂情况
- 规则引擎确保关键操作的确定性
这种方案在需要高可靠性的场景特别有用。
相比之下,本文介绍的Prompt Engineering方案在灵活性、实现成本和维护难度上取得了很好的平衡,特别适合快速迭代和中小规模的应用场景。
