feat: 增加工具调用
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"knowledge-graph-backend/internal/model"
|
||||
"log"
|
||||
|
||||
"github.com/tmc/langchaingo/llms"
|
||||
)
|
||||
|
||||
// KnowledgeGraphToolService 定义 LLM 客户端需要的图谱操作接口
|
||||
// service.Neo4jService 会隐式实现该接口,避免循环依赖
|
||||
type KnowledgeGraphToolService interface {
|
||||
SearchNodes(query string) []model.Node
|
||||
GetNeighbors(nodeID string) (model.NeighborResponse, bool)
|
||||
GetNodeByID(id string) (model.Node, bool)
|
||||
CreateNode(req model.CreateNodeRequest) (model.Node, error)
|
||||
CreateEdge(req model.CreateEdgeRequest) (model.Edge, error)
|
||||
DeleteNode(id string) error
|
||||
DeleteEdge(id string) error
|
||||
}
|
||||
|
||||
// Agent 封装了带有工具调用和上下文管理能力的智能体
|
||||
type Agent struct {
|
||||
llm llms.Model
|
||||
graphSvc KnowledgeGraphToolService
|
||||
}
|
||||
|
||||
// NewAgent 创建一个新的 Agent 实例
|
||||
func NewAgent(llm llms.Model, graphSvc KnowledgeGraphToolService) *Agent {
|
||||
return &Agent{
|
||||
llm: llm,
|
||||
graphSvc: graphSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// getKnowledgeGraphTools 声明 LLM 可以调用的工具列表及其 JSON Schema
|
||||
// (同原代码,此处省略重复注释)
|
||||
func getKnowledgeGraphTools() []llms.Tool {
|
||||
return []llms.Tool{
|
||||
{
|
||||
Type: "Node operation",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "search_nodes",
|
||||
Description: "当需要根据关键词在知识图谱中搜索相关节点时调用此工具",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"query": map[string]any{
|
||||
"type": "string",
|
||||
"description": "搜索的关键词",
|
||||
},
|
||||
},
|
||||
"required": []string{"query"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "Node operation",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "get_neighbors",
|
||||
Description: "当需要查找某个节点的相邻节点(即与该节点有直接连线的节点)时调用此工具",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"node_id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "目标节点的唯一 ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"node_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "Node operation",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "create_node",
|
||||
Description: "当需要在知识图谱中创建一个新的节点时调用此工具",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{"type": "string", "description": "节点的唯一ID,格式通常为 node_xxx"},
|
||||
"label": map[string]any{"type": "string", "description": "节点的名称/标签,例如:人工智能"},
|
||||
"type": map[string]any{"type": "string", "description": "节点的类型,例如:概念、实体"},
|
||||
"x": map[string]any{"type": "number", "description": "节点在画布上的 X 坐标 (可选,默认0)"},
|
||||
"y": map[string]any{"type": "number", "description": "节点在画布上的 Y 坐标 (可选,默认0)"},
|
||||
"properties": map[string]any{
|
||||
"type": "object",
|
||||
"description": "节点的额外属性键值对 (可选)",
|
||||
},
|
||||
},
|
||||
"required": []string{"id", "label", "type"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "Edge operation",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "create_edge",
|
||||
Description: "当需要在知识图谱中创建一条连线(边)连接两个节点时调用此工具",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{"type": "string", "description": "边的唯一ID,格式通常为 edge_xxx"},
|
||||
"source": map[string]any{"type": "string", "description": "起始节点的 ID"},
|
||||
"target": map[string]any{"type": "string", "description": "目标节点的 ID"},
|
||||
"label": map[string]any{"type": "string", "description": "连线的名称/标签,例如:包含、依赖于"},
|
||||
"type": map[string]any{"type": "string", "description": "连线的类型 (可选),例如:CONTAINS"},
|
||||
"properties": map[string]any{
|
||||
"type": "object",
|
||||
"description": "边的额外属性键值对 (可选)",
|
||||
},
|
||||
},
|
||||
"required": []string{"id", "source", "target", "label"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "Node operation",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "delete_node",
|
||||
Description: "当需要从知识图谱中删除一个现有节点时调用此工具",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "要删除的节点的唯一 ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "Edge operation",
|
||||
Function: &llms.FunctionDefinition{
|
||||
Name: "delete_edge",
|
||||
Description: "当需要从知识图谱中删除一条连线(边)时调用此工具",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "要删除的边的唯一 ID",
|
||||
},
|
||||
},
|
||||
"required": []string{"id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// executeToolCall 执行具体的工具调用逻辑
|
||||
// (同原代码,此处省略重复注释)
|
||||
func executeToolCall(toolCall llms.ToolCall, graphSvc KnowledgeGraphToolService) (string, error) {
|
||||
var result any
|
||||
var err error
|
||||
|
||||
switch toolCall.FunctionCall.Name {
|
||||
case "search_nodes":
|
||||
var args struct {
|
||||
Query string `json:"query"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments for search_nodes: %w", err)
|
||||
}
|
||||
result = graphSvc.SearchNodes(args.Query)
|
||||
|
||||
case "get_neighbors":
|
||||
var args struct {
|
||||
NodeID string `json:"node_id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments for get_neighbors: %w", err)
|
||||
}
|
||||
result, _ = graphSvc.GetNeighbors(args.NodeID)
|
||||
|
||||
case "create_node":
|
||||
var args model.CreateNodeRequest
|
||||
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments for create_node: %w", err)
|
||||
}
|
||||
result, err = graphSvc.CreateNode(args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
case "create_edge":
|
||||
var args model.CreateEdgeRequest
|
||||
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments for create_edge: %w", err)
|
||||
}
|
||||
result, err = graphSvc.CreateEdge(args)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
case "delete_node":
|
||||
var args struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments for delete_node: %w", err)
|
||||
}
|
||||
err = graphSvc.DeleteNode(args.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result = map[string]string{"status": "success", "message": fmt.Sprintf("Node %s deleted successfully", args.ID)}
|
||||
|
||||
case "delete_edge":
|
||||
var args struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
|
||||
return "", fmt.Errorf("invalid arguments for delete_edge: %w", err)
|
||||
}
|
||||
err = graphSvc.DeleteEdge(args.ID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result = map[string]string{"status": "success", "message": fmt.Sprintf("Edge %s deleted successfully", args.ID)}
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unknown tool: %s", toolCall.FunctionCall.Name)
|
||||
}
|
||||
|
||||
resBytes, _ := json.Marshal(result)
|
||||
return string(resBytes), nil
|
||||
}
|
||||
|
||||
// ChatWithTools 带有工具调用能力的 Agent 入口
|
||||
// 参考了 Client.go 的上下文管理模式,加入系统提示词及 Agent Loop 循环调用
|
||||
func (a *Agent) ChatWithTools(ctx context.Context, systemPrompt string, messages []llms.MessageContent) (string, error) {
|
||||
tools := getKnowledgeGraphTools()
|
||||
|
||||
// 1. 构造系统提示词消息,并插入到消息列表头部
|
||||
systemMessage := llms.TextParts(llms.ChatMessageTypeSystem, systemPrompt)
|
||||
allMessages := append([]llms.MessageContent{systemMessage}, messages...)
|
||||
|
||||
// 设定最大循环次数,防止死循环
|
||||
maxIterations := 10
|
||||
|
||||
for i := 0; i < maxIterations; i++ {
|
||||
// 2. 调用大模型,携带上下文和可用工具
|
||||
resp, err := a.llm.GenerateContent(ctx, allMessages, llms.WithTools(tools))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Agent GenerateContent failed: %v", err)
|
||||
return "", fmt.Errorf("agent generate content error: %w", err)
|
||||
}
|
||||
|
||||
if resp == nil || len(resp.Choices) == 0 {
|
||||
log.Printf("[WARN] Agent response is empty or has no choices")
|
||||
return "", fmt.Errorf("no content choices returned")
|
||||
}
|
||||
|
||||
choice := resp.Choices[0]
|
||||
|
||||
// 3. 如果没有工具调用,说明大模型已经生成了最终回复,退出循环
|
||||
if len(choice.ToolCalls) == 0 {
|
||||
log.Printf("[INFO] Agent chat completed successfully, choice length: %d", len(choice.Content))
|
||||
return choice.Content, nil
|
||||
}
|
||||
|
||||
// 4. 处理工具调用:需要将 AI 的工具调用指令追加到上下文中
|
||||
aiMessageParts := []llms.ContentPart{}
|
||||
if choice.Content != "" {
|
||||
aiMessageParts = append(aiMessageParts, llms.TextPart(choice.Content))
|
||||
}
|
||||
for _, tc := range choice.ToolCalls {
|
||||
aiMessageParts = append(aiMessageParts, tc)
|
||||
}
|
||||
allMessages = append(allMessages, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeAI,
|
||||
Parts: aiMessageParts,
|
||||
})
|
||||
|
||||
// 5. 依次执行每个工具调用,并将结果作为 Tool 消息追加到上下文
|
||||
for _, tc := range choice.ToolCalls {
|
||||
log.Printf("[INFO] Executing tool call: %s, Args: %s", tc.FunctionCall.Name, tc.FunctionCall.Arguments)
|
||||
|
||||
toolResult, err := executeToolCall(tc, a.graphSvc)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] Tool execution failed: %v", err)
|
||||
// 即使工具执行失败,也将错误信息返回给 LLM,让其进行自我修正或回复用户
|
||||
toolResult = fmt.Sprintf(`{"error": "%s"}`, err.Error())
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Tool call %s completed, result length: %d", tc.FunctionCall.Name, len(toolResult))
|
||||
|
||||
// 构造工具调用结果并追加到上下文历史
|
||||
allMessages = append(allMessages, llms.MessageContent{
|
||||
Role: llms.ChatMessageTypeTool,
|
||||
Parts: []llms.ContentPart{
|
||||
llms.ToolCallResponse{
|
||||
ToolCallID: tc.ID,
|
||||
Name: tc.FunctionCall.Name,
|
||||
Content: toolResult,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("agent reached maximum tool call iterations (%d)", maxIterations)
|
||||
}
|
||||
@@ -540,7 +540,7 @@ func (s *Neo4jService) UpdateNode(id string, req model.UpdateNodeRequest) (model
|
||||
query := `MATCH (n {id: $id})`
|
||||
|
||||
// 处理自定义属性(不管是否有标准字段更新)
|
||||
if req.Properties != nil && len(req.Properties) > 0 {
|
||||
if len(req.Properties) > 0 {
|
||||
for key, value := range req.Properties {
|
||||
// 跳过系统属性,避免冲突
|
||||
if key != "id" && key != "label" && key != "type" && key != "x" && key != "y" {
|
||||
|
||||
Reference in New Issue
Block a user