768 lines
23 KiB
Markdown
768 lines
23 KiB
Markdown
|
|
# 工具调用(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
|
|||
|
|
<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 工具调用可视化
|
|||
|
|
|
|||
|
|
在工作流执行时,显示工具调用过程:
|
|||
|
|
|
|||
|
|
```vue
|
|||
|
|
<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`
|
|||
|
|
|
|||
|
|
```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
|