feat: 增加工具调用

This commit is contained in:
2026-04-20 15:16:51 +08:00
parent b3e4ec4f4c
commit fe87f5ca80
2 changed files with 312 additions and 1 deletions
+311
View File
@@ -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)
}
+1 -1
View File
@@ -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" {