Files
aiagent/工具调用实现方案.md
2026-01-23 09:49:45 +08:00

23 KiB
Raw Blame History

工具调用Function Calling实现方案

📋 方案概述

本方案实现LLM工具调用功能允许LLM节点调用预定义的工具函数实现更强大的AI能力。

🎯 功能目标

  1. LLM节点支持工具调用

    • 在LLM节点配置中定义可用工具
    • LLM自动选择并调用合适的工具
    • 支持多轮工具调用Tool Calling Loop
  2. 工具定义和管理

    • 支持内置工具HTTP请求、数据库查询、文件操作等
    • 支持自定义工具Python函数、工作流节点等
    • 工具参数验证和类型转换
  3. 工具执行

    • 异步执行工具
    • 错误处理和重试
    • 结果格式化返回给LLM
  4. 前端配置界面

    • 工具选择器
    • 工具参数配置
    • 工具调用可视化

🏗️ 架构设计

1. 系统架构

┌─────────────────┐
│   LLM节点配置   │
│  (工具列表)     │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  LLM服务调用     │
│  (传递tools参数) │
└────────┬────────┘
         │
         ▼
┌─────────────────┐      ┌──────────────┐
│ 工具调用解析器   │─────▶│  工具执行器   │
│ (解析tool_call) │      │ (执行工具)    │
└────────┬────────┘      └──────┬───────┘
         │                      │
         │                      ▼
         │              ┌──────────────┐
         │              │  工具注册表   │
         │              │ (工具定义)    │
         │              └──────────────┘
         │
         ▼
┌─────────────────┐
│  结果返回LLM    │
│  (继续对话)     │
└─────────────────┘

2. 数据流

1. 用户输入 → LLM节点
2. LLM节点 → LLM API (带tools参数)
3. LLM API → 返回tool_call请求
4. 工具调用解析器 → 解析tool_call
5. 工具执行器 → 执行工具
6. 工具结果 → 返回LLM (tool message)
7. LLM → 生成最终回复

📝 实现步骤

阶段1: 后端核心功能

1.1 工具定义模型

文件: backend/app/models/tool.py

from sqlalchemy import Column, String, Text, JSON, DateTime, Boolean, ForeignKey, Integer
from sqlalchemy.dialects.mysql import CHAR
from sqlalchemy.orm import relationship
from app.core.database import Base
import uuid

class Tool(Base):
    """工具定义表"""
    __tablename__ = "tools"
    
    id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()))
    name = Column(String(100), nullable=False, unique=True, comment="工具名称")
    description = Column(Text, nullable=False, comment="工具描述")
    category = Column(String(50), comment="工具分类")
    
    # 工具定义OpenAI Function格式
    function_schema = Column(JSON, nullable=False, comment="函数定义JSON Schema")
    
    # 工具实现类型
    implementation_type = Column(String(50), nullable=False, comment="实现类型: builtin/http/workflow/code")
    implementation_config = Column(JSON, comment="实现配置")
    
    # 元数据
    is_public = Column(Boolean, default=False, comment="是否公开")
    user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="创建者ID")
    use_count = Column(Integer, default=0, comment="使用次数")
    created_at = Column(DateTime, default=func.now())
    updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
    
    user = relationship("User", backref="tools")

1.2 工具注册表

文件: backend/app/services/tool_registry.py

from typing import Dict, Any, Callable, Optional
import json
from app.models.tool import Tool
from sqlalchemy.orm import Session

class ToolRegistry:
    """工具注册表 - 管理所有可用工具"""
    
    def __init__(self):
        self._builtin_tools: Dict[str, Callable] = {}
        self._tool_schemas: Dict[str, Dict[str, Any]] = {}
    
    def register_builtin_tool(self, name: str, func: Callable, schema: Dict[str, Any]):
        """注册内置工具"""
        self._builtin_tools[name] = func
        self._tool_schemas[name] = schema
    
    def get_tool_schema(self, name: str) -> Optional[Dict[str, Any]]:
        """获取工具定义"""
        return self._tool_schemas.get(name)
    
    def get_tool_function(self, name: str) -> Optional[Callable]:
        """获取工具函数"""
        return self._builtin_tools.get(name)
    
    def get_all_tool_schemas(self) -> list:
        """获取所有工具定义用于LLM"""
        return list(self._tool_schemas.values())
    
    def load_tools_from_db(self, db: Session, tool_names: list = None):
        """从数据库加载工具"""
        query = db.query(Tool).filter(Tool.is_public == True)
        if tool_names:
            query = query.filter(Tool.name.in_(tool_names))
        
        tools = query.all()
        for tool in tools:
            self._tool_schemas[tool.name] = tool.function_schema
            # 根据implementation_type加载工具实现
            if tool.implementation_type == 'builtin':
                # 从内置工具中查找
                if tool.name in self._builtin_tools:
                    pass  # 已注册
            elif tool.implementation_type == 'http':
                # HTTP工具需要特殊处理
                self._register_http_tool(tool)
            elif tool.implementation_type == 'workflow':
                # 工作流工具
                self._register_workflow_tool(tool)
            elif tool.implementation_type == 'code':
                # 代码执行工具
                self._register_code_tool(tool)

# 全局工具注册表实例
tool_registry = ToolRegistry()

1.3 内置工具实现

文件: backend/app/services/builtin_tools.py

from typing import Dict, Any
import httpx
import json

async def http_request_tool(url: str, method: str = "GET", headers: Dict = None, body: Any = None) -> str:
    """HTTP请求工具"""
    try:
        async with httpx.AsyncClient() as client:
            if method.upper() == "GET":
                response = await client.get(url, headers=headers)
            elif method.upper() == "POST":
                response = await client.post(url, json=body, headers=headers)
            else:
                raise ValueError(f"不支持的HTTP方法: {method}")
            
            return json.dumps({
                "status_code": response.status_code,
                "headers": dict(response.headers),
                "body": response.text
            }, ensure_ascii=False)
    except Exception as e:
        return json.dumps({"error": str(e)}, ensure_ascii=False)

async def database_query_tool(query: str, database: str = "default") -> str:
    """数据库查询工具"""
    # 实现数据库查询逻辑
    pass

async def file_read_tool(file_path: str) -> str:
    """文件读取工具"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        return f"错误: {str(e)}"

# 工具定义OpenAI Function格式
HTTP_REQUEST_SCHEMA = {
    "type": "function",
    "function": {
        "name": "http_request",
        "description": "发送HTTP请求支持GET和POST方法",
        "parameters": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "请求URL"
                },
                "method": {
                    "type": "string",
                    "enum": ["GET", "POST", "PUT", "DELETE"],
                    "description": "HTTP方法"
                },
                "headers": {
                    "type": "object",
                    "description": "请求头"
                },
                "body": {
                    "type": "object",
                    "description": "请求体POST/PUT时使用"
                }
            },
            "required": ["url", "method"]
        }
    }
}

FILE_READ_SCHEMA = {
    "type": "function",
    "function": {
        "name": "file_read",
        "description": "读取文件内容",
        "parameters": {
            "type": "object",
            "properties": {
                "file_path": {
                    "type": "string",
                    "description": "文件路径"
                }
            },
            "required": ["file_path"]
        }
    }
}

1.4 LLM服务扩展

文件: backend/app/services/llm_service.py (扩展)

from typing import List, Dict, Any, Optional
from app.services.tool_registry import tool_registry

class LLMService:
    # ... 现有代码 ...
    
    async def call_openai_with_tools(
        self,
        prompt: str,
        tools: List[Dict[str, Any]],
        model: str = "gpt-3.5-turbo",
        temperature: float = 0.7,
        max_tokens: Optional[int] = None,
        api_key: Optional[str] = None,
        base_url: Optional[str] = None,
        max_iterations: int = 5
    ) -> str:
        """
        调用OpenAI API支持工具调用
        
        Args:
            prompt: 提示词
            tools: 工具定义列表OpenAI Function格式
            model: 模型名称
            temperature: 温度参数
            max_tokens: 最大token数
            api_key: API密钥
            base_url: API地址
            max_iterations: 最大工具调用迭代次数
        
        Returns:
            LLM返回的最终文本
        """
        messages = [{"role": "user", "content": prompt}]
        
        for iteration in range(max_iterations):
            # 调用LLM
            response = await client.chat.completions.create(
                model=model,
                messages=messages,
                tools=tools if iteration == 0 else None,  # 只在第一次调用时传递tools
                tool_choice="auto",
                temperature=temperature,
                max_tokens=max_tokens
            )
            
            message = response.choices[0].message
            
            # 添加助手回复到消息历史
            messages.append(message)
            
            # 检查是否有工具调用
            if message.tool_calls:
                # 处理每个工具调用
                for tool_call in message.tool_calls:
                    tool_name = tool_call.function.name
                    tool_args = json.loads(tool_call.function.arguments)
                    
                    # 执行工具
                    tool_result = await self._execute_tool(tool_name, tool_args)
                    
                    # 添加工具结果到消息历史
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "content": tool_result
                    })
            else:
                # 没有工具调用,返回最终回复
                return message.content or ""
        
        # 达到最大迭代次数
        return messages[-1].get("content", "达到最大工具调用次数")
    
    async def _execute_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> str:
        """执行工具"""
        # 从注册表获取工具函数
        tool_func = tool_registry.get_tool_function(tool_name)
        
        if not tool_func:
            return json.dumps({"error": f"工具 {tool_name} 未找到"}, ensure_ascii=False)
        
        try:
            # 执行工具(支持异步函数)
            if asyncio.iscoroutinefunction(tool_func):
                result = await tool_func(**tool_args)
            else:
                result = tool_func(**tool_args)
            
            # 将结果转换为字符串
            if isinstance(result, (dict, list)):
                return json.dumps(result, ensure_ascii=False)
            return str(result)
        except Exception as e:
            return json.dumps({"error": str(e)}, ensure_ascii=False)

1.5 工作流引擎扩展

文件: backend/app/services/workflow_engine.py (扩展)

# 在LLM节点执行部分添加工具调用支持

elif node_type == 'llm' or node_type == 'template':
    node_data = node.get('data', {})
    prompt = node_data.get('prompt', '')
    
    # 获取工具配置
    tools_config = node_data.get('tools', [])  # 工具名称列表
    enable_tools = node_data.get('enable_tools', False)
    
    # 如果启用了工具,加载工具定义
    tools = []
    if enable_tools and tools_config:
        # 从注册表加载工具定义
        for tool_name in tools_config:
            tool_schema = tool_registry.get_tool_schema(tool_name)
            if tool_schema:
                tools.append(tool_schema)
    
    # 调用LLM带工具
    if tools:
        result = await llm_service.call_openai_with_tools(
            prompt=formatted_prompt,
            tools=tools,
            provider=provider,
            model=model,
            temperature=temperature,
            max_tokens=max_tokens
        )
    else:
        # 普通调用
        result = await llm_service.call_llm(
            prompt=formatted_prompt,
            provider=provider,
            model=model,
            temperature=temperature,
            max_tokens=max_tokens
        )

阶段2: 数据库迁移

文件: backend/alembic/versions/xxxx_add_tools_table.py

def upgrade():
    op.create_table(
        'tools',
        sa.Column('id', sa.CHAR(36), primary_key=True),
        sa.Column('name', sa.String(100), nullable=False, unique=True),
        sa.Column('description', sa.Text, nullable=False),
        sa.Column('category', sa.String(50)),
        sa.Column('function_schema', sa.JSON, nullable=False),
        sa.Column('implementation_type', sa.String(50), nullable=False),
        sa.Column('implementation_config', sa.JSON),
        sa.Column('is_public', sa.Boolean, default=False),
        sa.Column('user_id', sa.CHAR(36), sa.ForeignKey('users.id')),
        sa.Column('use_count', sa.Integer, default=0),
        sa.Column('created_at', sa.DateTime, default=func.now()),
        sa.Column('updated_at', sa.DateTime, default=func.now(), onupdate=func.now())
    )

阶段3: API接口

文件: backend/app/api/tools.py

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.models.tool import Tool
from app.services.tool_registry import tool_registry
from app.api.auth import get_current_user

router = APIRouter(prefix="/api/v1/tools", tags=["tools"])

@router.get("")
async def list_tools(
    category: str = None,
    db: Session = Depends(get_db)
):
    """获取工具列表"""
    query = db.query(Tool).filter(Tool.is_public == True)
    if category:
        query = query.filter(Tool.category == category)
    
    tools = query.all()
    return [{
        "id": tool.id,
        "name": tool.name,
        "description": tool.description,
        "category": tool.category,
        "function_schema": tool.function_schema
    } for tool in tools]

@router.post("")
async def create_tool(
    tool_data: dict,
    db: Session = Depends(get_db),
    current_user = Depends(get_current_user)
):
    """创建工具"""
    tool = Tool(
        name=tool_data["name"],
        description=tool_data["description"],
        category=tool_data.get("category"),
        function_schema=tool_data["function_schema"],
        implementation_type=tool_data["implementation_type"],
        implementation_config=tool_data.get("implementation_config"),
        user_id=current_user.id
    )
    db.add(tool)
    db.commit()
    return tool

@router.get("/builtin")
async def list_builtin_tools():
    """获取内置工具列表"""
    schemas = tool_registry.get_all_tool_schemas()
    return schemas

阶段4: 前端实现

4.1 LLM节点配置扩展

文件: frontend/src/components/WorkflowEditor/WorkflowEditor.vue

<template>
  <!-- 在LLM节点配置面板中添加工具配置 -->
  <el-tab-pane label="工具" name="tools" v-if="selectedNode.type === 'llm'">
    <el-switch
      v-model="selectedNode.data.enable_tools"
      label="启用工具调用"
    />
    
    <div v-if="selectedNode.data.enable_tools" style="margin-top: 20px;">
      <el-select
        v-model="selectedNode.data.tools"
        multiple
        placeholder="选择可用工具"
        style="width: 100%"
      >
        <el-option
          v-for="tool in availableTools"
          :key="tool.name"
          :label="tool.name"
          :value="tool.name"
        >
          <div>
            <strong>{{ tool.name }}</strong>
            <p style="margin: 0; color: #999; font-size: 12px;">
              {{ tool.description }}
            </p>
          </div>
        </el-option>
      </el-select>
      
      <div v-for="toolName in selectedNode.data.tools" :key="toolName" style="margin-top: 10px;">
        <el-card>
          <template #header>
            <span>{{ toolName }}</span>
          </template>
          <div v-if="getToolSchema(toolName)">
            <p><strong>描述:</strong> {{ getToolSchema(toolName).function.description }}</p>
            <p><strong>参数:</strong></p>
            <pre>{{ JSON.stringify(getToolSchema(toolName).function.parameters, null, 2) }}</pre>
          </div>
        </el-card>
      </div>
    </div>
  </el-tab-pane>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import api from '@/api'

const availableTools = ref([])
const toolSchemas = ref({})

onMounted(async () => {
  // 加载可用工具
  const response = await api.get('/api/v1/tools')
  availableTools.value = response.data
  
  // 加载内置工具
  const builtinResponse = await api.get('/api/v1/tools/builtin')
  availableTools.value = [...availableTools.value, ...builtinResponse.data]
  
  // 构建工具schema映射
  availableTools.value.forEach(tool => {
    if (tool.function_schema) {
      toolSchemas.value[tool.name] = tool.function_schema
    }
  })
})

const getToolSchema = (toolName: string) => {
  return toolSchemas.value[toolName]
}
</script>

4.2 工具调用可视化

在工作流执行时,显示工具调用过程:

<template>
  <div class="tool-call-visualization" v-if="toolCalls.length > 0">
    <h4>工具调用过程:</h4>
    <div v-for="(call, index) in toolCalls" :key="index" class="tool-call-item">
      <div class="tool-call-header">
        <span class="tool-name">{{ call.name }}</span>
        <span class="tool-status" :class="call.status">
          {{ call.status === 'success' ? '✓' : '✗' }}
        </span>
      </div>
      <div class="tool-call-args">
        <strong>参数:</strong>
        <pre>{{ JSON.stringify(call.arguments, null, 2) }}</pre>
      </div>
      <div class="tool-call-result" v-if="call.result">
        <strong>结果:</strong>
        <pre>{{ call.result }}</pre>
      </div>
    </div>
  </div>
</template>

阶段5: 初始化内置工具

文件: backend/scripts/init_builtin_tools.py

from app.core.database import SessionLocal
from app.models.tool import Tool
from app.services.tool_registry import tool_registry
from app.services.builtin_tools import (
    http_request_tool,
    file_read_tool,
    HTTP_REQUEST_SCHEMA,
    FILE_READ_SCHEMA
)

def init_builtin_tools():
    """初始化内置工具"""
    db = SessionLocal()
    
    try:
        # 注册内置工具到注册表
        tool_registry.register_builtin_tool(
            "http_request",
            http_request_tool,
            HTTP_REQUEST_SCHEMA
        )
        
        tool_registry.register_builtin_tool(
            "file_read",
            file_read_tool,
            FILE_READ_SCHEMA
        )
        
        # 保存到数据库
        for tool_name, tool_schema in [
            ("http_request", HTTP_REQUEST_SCHEMA),
            ("file_read", FILE_READ_SCHEMA)
        ]:
            existing = db.query(Tool).filter(Tool.name == tool_name).first()
            if not existing:
                tool = Tool(
                    name=tool_name,
                    description=tool_schema["function"]["description"],
                    category="builtin",
                    function_schema=tool_schema,
                    implementation_type="builtin",
                    is_public=True
                )
                db.add(tool)
        
        db.commit()
        print("✅ 内置工具初始化完成")
    except Exception as e:
        db.rollback()
        print(f"❌ 初始化失败: {str(e)}")
    finally:
        db.close()

if __name__ == "__main__":
    init_builtin_tools()

📊 使用示例

示例1: 配置LLM节点使用工具

{
  "id": "llm-1",
  "type": "llm",
  "data": {
    "label": "智能助手",
    "provider": "openai",
    "model": "gpt-4",
    "prompt": "请帮助用户解决问题,可以使用工具获取信息。",
    "enable_tools": true,
    "tools": ["http_request", "file_read"]
  }
}

示例2: 工作流示例

开始 → LLM节点(启用工具) → 结束

用户输入: "查询北京的天气"

  1. LLM识别需要调用工具
  2. 调用 http_request 工具查询天气API
  3. 获取结果后生成回复

🔒 安全考虑

  1. 工具权限控制

    • 限制可执行的文件路径
    • 限制HTTP请求的目标域名
    • 限制数据库查询权限
  2. 参数验证

    • 验证工具参数类型和范围
    • 防止注入攻击
  3. 执行超时

    • 设置工具执行超时时间
    • 防止无限循环
  4. 资源限制

    • 限制工具调用次数
    • 限制工具执行时间

📈 后续优化

  1. 工具市场

    • 用户可分享自定义工具
    • 工具评分和评论
  2. 工具组合

    • 支持工具链(一个工具调用另一个工具)
    • 工具依赖管理
  3. 性能优化

    • 工具结果缓存
    • 并行工具执行
  4. 监控和日志

    • 工具调用统计
    • 工具执行日志
    • 性能分析

🎯 实施优先级

高优先级MVP

  1. 工具注册表
  2. 内置工具HTTP请求、文件读取
  3. LLM服务工具调用支持
  4. 工作流引擎集成
  5. 前端工具配置界面

中优先级

  1. 数据库工具
  2. 工具调用可视化
  3. 工具执行日志
  4. 错误处理和重试

低优先级

  1. 工具市场
  2. 工具组合
  3. 性能优化
  4. 高级安全控制

方案版本: v1.0
创建时间: 2026-01-23
作者: AI Assistant