Files
aiagent/工具调用实现方案.md

768 lines
23 KiB
Markdown
Raw Permalink Normal View History

2026-01-23 09:49:45 +08:00
# 工具调用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