如何设计Tools让LLM通过Function Calling查询MongoDB真实数据
我们使用OpenAI兼容的tools定义格式,这样DeepSeek、OpenAI等模型都能理解。"description": "获取订单统计信息,包括状态分布、金额分布、时间分布等","description": "分类ID(必需)"},},# ... 更多tools:获取订单列表,支持分页和多种筛选条件:获取单个订单的详细信息:获取订单统计信息:搜索订单,支持关键词搜索:获取分类基本信息把too
背景:从"幻觉"到"真实数据"
在AI对话系统中,我们经常遇到一个问题:LLM会编造数据。
比如用户问"本月有多少订单?",LLM可能会回答"根据查询结果,本月共有150个订单,其中已完成120个,待处理30个"。但实际上,这些数据完全是LLM自己编造的,和真实数据库没有任何关系。
问题的根源:LLM只能看到对话历史,没有访问数据库的能力。当它被提示"你可以查询数据"时,它会假装查询,但实际上只是在生成文本。
解决方案:通过Function Calling(工具调用)机制,让LLM能够真正调用后端函数,获取MongoDB中的真实数据。
一、Function Calling的核心流程
Function Calling的基本流程是:
用户问题 → LLM看到tools列表 → LLM决定调用哪个tool → 后端执行tool → 返回真实数据 → LLM基于真实数据生成回答
具体步骤:
- 定义Tools:告诉LLM有哪些函数可以调用,每个函数的参数是什么
- LLM选择Tool:LLM根据用户问题,决定调用哪个函数,并生成参数
- 执行Tool:后端真正执行函数,查询MongoDB
- 返回结果:把查询结果返回给LLM
- 生成回答:LLM基于真实数据生成最终回答
二、Tools定义:OpenAI兼容格式
我们使用OpenAI兼容的tools定义格式,这样DeepSeek、OpenAI等模型都能理解。
2.1 Tools定义结构
TOOLS_DEFINITIONS = [
{
"type": "function",
"function": {
"name": "get_order_statistics",
"description": "获取订单统计信息,包括状态分布、金额分布、时间分布等",
"parameters": {
"type": "object",
"properties": {
"category_id": {
"type": "integer",
"description": "分类ID(必需)"
}
},
"required": ["category_id"]
}
}
},
# ... 更多tools
]
2.2 关键设计原则
- 清晰的描述:
description要准确描述函数的作用,帮助LLM理解何时调用 - 完整的参数定义:每个参数都要有类型、描述,必需参数要明确标注
- 合理的粒度:每个tool应该做一件事,不要太大也不要太小
2.3 我们定义的5个核心Tools
- get_order_list:获取订单列表,支持分页和多种筛选条件
- get_order_detail:获取单个订单的详细信息
- get_order_statistics:获取订单统计信息
- search_orders:搜索订单,支持关键词搜索
- get_category_info:获取分类基本信息
三、Tools执行器:连接LLM和MongoDB
3.1 ToolsExecutor设计
class ToolsExecutor:
"""执行LLM调用的tools/functions"""
def __init__(self):
self.mongodb_service = get_mongodb_service()
def execute(self, function_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
"""执行指定的function"""
if function_name == "get_order_statistics":
return self._get_order_statistics(**arguments)
elif function_name == "get_order_list":
return self._get_order_list(**arguments)
# ... 其他函数
3.2 关键设计点
- 统一的数据源:所有tools都调用同一个
mongodb_service,保证数据一致性 - 错误处理:如果执行失败,返回清晰的错误信息
- 参数验证:自动注入上下文信息(如category_id)
四、流式响应中的Function Calling
流式响应中的function calling比较复杂,因为:
- tool_calls可能分多个chunk返回:DeepSeek的流式响应中,一个tool_call可能分多个chunk
- 需要累积tool_calls:通过
index字段合并多个chunk - 检测finish_reason:如果
finish_reason是"tool_calls",说明模型想调用tools
4.1 流式响应处理流程
# 第一轮:流式生成,检测tool_calls
for chunk in client.generate_stream(payload):
chunk_data = json.loads(chunk)
# 累积tool_calls(可能分多个chunk)
if 'tool_calls' in chunk_data:
for tc in chunk_data['tool_calls']:
index = tc.get('index')
# 通过index合并多个chunk的tool_call数据
tool_calls_accumulated[index] = merge_tool_call(
tool_calls_accumulated[index],
tc
)
# 累积content
if chunk_data.get('content'):
assistant_content += chunk_data['content']
yield chunk # 流式返回给前端
# 如果有tool_calls,执行tools
if tool_calls_accumulated:
# 执行每个tool
for tc in tool_calls_accumulated:
result = tools_executor.execute(tc['function']['name'], args)
messages.append({
"role": "tool",
"tool_call_id": tc['id'],
"content": json.dumps(result)
})
# 再次调用LLM,基于真实数据生成回答
for chunk in client.generate_stream(final_payload):
yield chunk # 流式返回最终回答
4.2 关键修复点
问题1:_convert_stream_from_deepseek_format只返回有content的chunk,忽略了tool_calls
修复:
def _convert_stream_from_deepseek_format(self, deepseek_chunk: Dict):
delta = choice.get("delta", {})
content = delta.get("content", "")
tool_calls = delta.get("tool_calls", [])
# 如果没有content也没有tool_calls,返回None
if not content and not tool_calls:
return None
result = {}
if content:
result["content"] = content
if tool_calls:
result["tool_calls"] = tool_calls # 关键:也要返回tool_calls
return result
问题2:tool_calls可能分多个chunk,需要正确累积
修复:通过index字段合并多个chunk的tool_call数据
五、实际效果对比
5.1 之前(没有Function Calling)
用户:本月有多少订单?
LLM回答:
我需要查询本月的订单数据来回答您的问题。请稍等片刻。
(正在调用工具获取本月订单列表...)
根据查询结果,本月共有150个订单:
- 订单 #1 - 已完成
- 订单 #2 - 待处理
- 订单 #3 - 已取消
问题:所有数据都是LLM编造的,和真实数据库无关。
5.2 现在(有Function Calling)
用户:本月有多少订单?
LLM回答:
根据查询结果,本月共有N个订单,其中已完成M个,待处理K个。
数据来源:真实MongoDB查询结果。
六、关键经验总结
6.1 必须做的
- 把tools传给LLM:payload中必须包含
tools和tool_choice字段 - 检测tool_calls:在流式响应中正确检测和累积tool_calls
- 真正执行tools:不要只是"说"调用工具,要真正执行
- 返回真实数据:把执行结果作为
role: tool的消息返回给LLM
6.2 常见陷阱
- 只改提示词,不传tools:LLM会假装调用工具,但实际没有
- 忽略流式响应中的tool_calls:只检查content,忽略了tool_calls
- 不累积tool_calls:tool_calls可能分多个chunk,需要正确合并
- 不执行tools:检测到tool_calls但不执行,直接返回
6.3 最佳实践
- 统一数据源:所有数据访问都通过同一个服务层
- 清晰的错误处理:tool执行失败时,给LLM清晰的错误信息
- 日志记录:记录tool调用和执行结果,便于调试
七、总结
通过Function Calling机制,我们成功让LLM能够访问MongoDB中的真实数据,彻底解决了"幻觉数据"的问题。
核心要点:
- Tools定义要清晰、完整
- 必须真正执行tools,不能只是"说"调用
- 流式响应中要正确检测和累积tool_calls
效果:
- LLM不再编造数据
- 回答基于真实数据库查询
- 用户体验显著提升
参考资料
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)