From fe87f5ca8053982181f1501a982123798c03e79d Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 20 Apr 2026 15:16:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/llm/agent.go | 311 ++++++++++++++++++++++ backend/internal/service/neo4j_service.go | 2 +- 2 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 backend/internal/llm/agent.go diff --git a/backend/internal/llm/agent.go b/backend/internal/llm/agent.go new file mode 100644 index 0000000..1c818b1 --- /dev/null +++ b/backend/internal/llm/agent.go @@ -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) +} \ No newline at end of file diff --git a/backend/internal/service/neo4j_service.go b/backend/internal/service/neo4j_service.go index 6590f9a..9aa6ac5 100644 --- a/backend/internal/service/neo4j_service.go +++ b/backend/internal/service/neo4j_service.go @@ -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" {