背景:从"幻觉"到"真实数据"

在AI对话系统中,我们经常遇到一个问题:LLM会编造数据

比如用户问"本月有多少订单?",LLM可能会回答"根据查询结果,本月共有150个订单,其中已完成120个,待处理30个"。但实际上,这些数据完全是LLM自己编造的,和真实数据库没有任何关系。

问题的根源:LLM只能看到对话历史,没有访问数据库的能力。当它被提示"你可以查询数据"时,它会假装查询,但实际上只是在生成文本。

解决方案:通过Function Calling(工具调用)机制,让LLM能够真正调用后端函数,获取MongoDB中的真实数据。


一、Function Calling的核心流程

Function Calling的基本流程是:

用户问题 → LLM看到tools列表 → LLM决定调用哪个tool → 后端执行tool → 返回真实数据 → LLM基于真实数据生成回答

具体步骤:

  1. 定义Tools:告诉LLM有哪些函数可以调用,每个函数的参数是什么
  2. LLM选择Tool:LLM根据用户问题,决定调用哪个函数,并生成参数
  3. 执行Tool:后端真正执行函数,查询MongoDB
  4. 返回结果:把查询结果返回给LLM
  5. 生成回答: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 关键设计原则

  1. 清晰的描述description要准确描述函数的作用,帮助LLM理解何时调用
  2. 完整的参数定义:每个参数都要有类型、描述,必需参数要明确标注
  3. 合理的粒度:每个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 关键设计点

  1. 统一的数据源:所有tools都调用同一个mongodb_service,保证数据一致性
  2. 错误处理:如果执行失败,返回清晰的错误信息
  3. 参数验证:自动注入上下文信息(如category_id)

四、流式响应中的Function Calling

流式响应中的function calling比较复杂,因为:

  1. tool_calls可能分多个chunk返回:DeepSeek的流式响应中,一个tool_call可能分多个chunk
  2. 需要累积tool_calls:通过index字段合并多个chunk
  3. 检测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 必须做的

  1. 把tools传给LLM:payload中必须包含toolstool_choice字段
  2. 检测tool_calls:在流式响应中正确检测和累积tool_calls
  3. 真正执行tools:不要只是"说"调用工具,要真正执行
  4. 返回真实数据:把执行结果作为role: tool的消息返回给LLM

6.2 常见陷阱

  1. 只改提示词,不传tools:LLM会假装调用工具,但实际没有
  2. 忽略流式响应中的tool_calls:只检查content,忽略了tool_calls
  3. 不累积tool_calls:tool_calls可能分多个chunk,需要正确合并
  4. 不执行tools:检测到tool_calls但不执行,直接返回

6.3 最佳实践

  1. 统一数据源:所有数据访问都通过同一个服务层
  2. 清晰的错误处理:tool执行失败时,给LLM清晰的错误信息
  3. 日志记录:记录tool调用和执行结果,便于调试

七、总结

通过Function Calling机制,我们成功让LLM能够访问MongoDB中的真实数据,彻底解决了"幻觉数据"的问题。

核心要点

  • Tools定义要清晰、完整
  • 必须真正执行tools,不能只是"说"调用
  • 流式响应中要正确检测和累积tool_calls

效果

  • LLM不再编造数据
  • 回答基于真实数据库查询
  • 用户体验显著提升

参考资料

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐