# 工具调用(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` ```python 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` ```python 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` ```python 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` (扩展) ```python 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` (扩展) ```python # 在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` ```python 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` ```python 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` ```vue {{ tool.name }} {{ tool.description }} {{ toolName }} 描述: {{ getToolSchema(toolName).function.description }} 参数: {{ JSON.stringify(getToolSchema(toolName).function.parameters, null, 2) }} ``` #### 4.2 工具调用可视化 在工作流执行时,显示工具调用过程: ```vue 工具调用过程: {{ call.name }} {{ call.status === 'success' ? '✓' : '✗' }} 参数: {{ JSON.stringify(call.arguments, null, 2) }} 结果: {{ call.result }} ``` ### 阶段5: 初始化内置工具 **文件**: `backend/scripts/init_builtin_tools.py` ```python 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节点使用工具 ```json { "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
{{ tool.description }}
描述: {{ getToolSchema(toolName).function.description }}
参数:
{{ JSON.stringify(getToolSchema(toolName).function.parameters, null, 2) }}
{{ JSON.stringify(call.arguments, null, 2) }}
{{ call.result }}