21种AI智能体(Agent)设计模式-B组:执行与任务编排模式-LangChain4j实现例子

2、B 组:与外部世界交互

2.1、模式 5:工具使用(Tool Use)
2.1.1、示例
package com.penngo.agents.agent.b;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.penngo.agents.test.ApiKeys;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.Map;

import static java.time.Duration.ofSeconds;

/**
 * 模式 5:工具使用(Tool Use / Function Calling)
 * <p>
 * 让 LLM 在需要时生成结构化工具调用请求,由 Java 方法执行外部能力,再把观察结果交回 LLM 处理:
 * <pre>
 * [LLM 决策] → 结构化调用请求 → [@Tool Java 方法执行] → 观察结果 → [LLM 生成最终回答]
 * </pre>
 * <p>
 * 本示例提供三类生产智能体常见工具:
 * <ul>
 *   <li>精确计算:{@link CustomerTools#calculate(String, double, double)},弥补 LLM 心算不稳定;</li>
 *   <li>私有数据:{@link CustomerTools#queryOrder(String)},模拟查询内部订单数据库;</li>
 *   <li>环境信息:{@link CustomerTools#currentTime()},获取当前系统时间。</li>
 * </ul>
 * LangChain4j 通过 {@link Tool} / {@link P} 注解生成工具规格,OpenAI 兼容模型通过 Function Calling 决策何时调用。
 */
public class ToolAgent {

    /** 对外暴露的助手接口。模型会按需调用 tools(...) 注册的工具。 */
    public interface CustomerAssistant {
        @SystemMessage("你是一个客服智能体。你可以在需要时调用工具:" +
                "1. 涉及数学计算时,必须使用 calculator 工具,不要心算;" +
                "2. 涉及订单状态、物流、金额时,必须使用 queryOrder 工具;" +
                "3. 涉及当前时间、今天日期时,必须使用 currentTime 工具。" +
                "工具返回后,结合观察结果用中文给用户最终回答。")
        @UserMessage("{{question}}")
        String chat(@V("question") String question);
    }

    /**
     * 工具集合:每个 public 且带 {@link Tool} 的方法都会变成一个 Function Calling 工具。
     * <p>
     * 工具描述要清晰说明用途、参数与限制;参数描述用 {@link P} 标注,帮助 LLM 正确组装调用参数。
     */
    public static class CustomerTools {

        /** 模拟私有订单数据库。真实项目中可替换为 JDBC / HTTP API / 内部 RPC。 */
        private final Map<String, Order> orders = Map.of(
                "A1001", new Order("A1001", "已发货", "SF123456789", "Java 编程思想", "89.00"),
                "A1002", new Order("A1002", "退款中", "", "GPU 服务器定金", "200000.00"),
                "A1003", new Order("A1003", "已签收", "YT987654321", "显示器", "1299.00")
        );

        @Tool("查询内部订单信息。适用于用户询问订单状态、物流单号、商品名称、金额、退款进度等私有数据。参数 orderNo 必须是订单号,例如 A1001。返回 JSON 字符串。")
        public String queryOrder(@P("订单号,例如 A1001、A1002") String orderNo) {
            System.out.println("[Tool] queryOrder(orderNo=" + orderNo + ")");
            Order order = orders.get(orderNo);
            if (order == null) {
                return JSONUtil.toJsonStr(Map.of(
                        "found", false,
                        "message", "未找到订单,请用户核对订单号"
                ));
            }
            return JSONUtil.toJsonStr(Map.of(
                    "found", true,
                    "orderNo", order.orderNo(),
                    "status", order.status(),
                    "trackingNo", order.trackingNo(),
                    "product", order.product(),
                    "amount", order.amount()
            ));
        }

        @Tool("执行精确的二元数学计算。适用于加、减、乘、除等需要准确结果的问题。operation 只能是 add/subtract/multiply/divide。返回计算结果字符串。")
        public String calculate(@P("运算类型:add/subtract/multiply/divide") String operation,
                                @P("第一个数字") double a,
                                @P("第二个数字") double b) {
            System.out.println("[Tool] calculate(operation=" + operation + ", a=" + a + ", b=" + b + ")");
            BigDecimal x = BigDecimal.valueOf(a);
            BigDecimal y = BigDecimal.valueOf(b);
            BigDecimal result = switch (operation) {
                case "add" -> x.add(y);
                case "subtract" -> x.subtract(y);
                case "multiply" -> x.multiply(y);
                case "divide" -> {
                    if (BigDecimal.ZERO.compareTo(y) == 0) {
                        throw new IllegalArgumentException("除数不能为 0");
                    }
                    yield x.divide(y, 8, RoundingMode.HALF_UP).stripTrailingZeros();
                }
                default -> throw new IllegalArgumentException("不支持的 operation:" + operation);
            };
            return result.stripTrailingZeros().toPlainString();
        }

        @Tool("获取当前系统时间。适用于用户询问现在几点、今天日期、当前时间等最新环境信息。返回格式 yyyy-MM-dd HH:mm:ss。")
        public String currentTime() {
            System.out.println("[Tool] currentTime()");
            return DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
        }
    }

    /** 模拟数据库表里的订单记录。 */
    public record Order(String orderNo, String status, String trackingNo, String product, String amount) {}

    private final CustomerAssistant assistant;

    public ToolAgent(ChatModel model) {
        this.assistant = AiServices.builder(CustomerAssistant.class)
                .chatModel(model)
                .tools(new CustomerTools())
                .build();
    }

    public String chat(String question) {
        System.out.println("\n[User] " + question);
        String answer = assistant.chat(question);
        System.out.println("[Assistant] " + answer);
        return answer;
    }

    public static void main(String[] args) {
        ChatModel model = OpenAiChatModel.builder()
//                .baseUrl(ApiKeys.API_URL)
//                .apiKey(ApiKeys.OPENAI_API_KEY)
//                .modelName(ApiKeys.MODEL_NAME)
                .baseUrl(ApiKeys.API_URL_VOLCENGINE)
                .apiKey(ApiKeys.OPENAI_API_KEY_VOLCENGINE)
                .modelName(ApiKeys.MODEL_NAME_VOLCENGINE)
                .temperature(0.2)
                .timeout(ofSeconds(120))
                .logRequests(true)
                .logResponses(true)
                .build();

        ToolAgent agent = new ToolAgent(model);

        System.out.println("=== Tool Use 开始:" +
                DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss") + " ===");

        // 1. 精确计算:应触发 calculator 工具
        agent.chat("89.9 乘以 37 再加 100,大概是多少钱?请给准确结果。正好验证一下");

        // 2. 私有数据:应触发 queryOrder 工具
        agent.chat("帮我查一下订单 A1001 发货了吗?物流单号是多少?");

        // 3. 环境信息:应触发 currentTime 工具
        agent.chat("现在是什么时间?");

        // 4. 普通闲聊:不需要工具,LLM 直接回答
        agent.chat("你能做什么?");

        System.out.println("=== Tool Use 结束:" +
                DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss") + " ===");
    }
}

2.1.2、运行结果
=== Tool Use 开始:2026-06-28 22:03:47 ===

[User] 89.9 乘以 37 再加 100,大概是多少钱?请给准确结果。正好验证一下
[Tool] calculate(operation=multiply, a=89.9, b=37.0)
[Tool] calculate(operation=add, a=3326.3, b=100.0)
[Assistant] 准确结果是 **3426.3 元**。

[User] 帮我查一下订单 A1001 发货了吗?物流单号是多少?
[Tool] queryOrder(orderNo=A1001)
[Assistant] 订单 A1001 已发货。

- 商品:Java 编程思想
- 物流单号:SF123456789

[User] 现在是什么时间?
[Tool] currentTime()
[Assistant] 现在是 2026-06-28 22:04:22。

[User] 你能做什么?
[Assistant] 我可以帮你处理以下问题:

1. **订单查询**:查询订单状态、物流单号、商品名称、金额、退款进度等。  
2. **数学计算**:加、减、乘、除等需要准确结果的计算。  
3. **时间日期查询**:查询当前时间、今天日期。  
4. **客服咨询**:根据你提供的信息,帮你整理问题、解释查询结果、给出下一步建议。

你可以直接问我,比如:  
- “帮我查一下订单 A1001”  
- “现在几点?”  
- “123.45 乘以 6 等于多少?”
=== Tool Use 结束:2026-06-28 22:04:27 ===
2.2、模式 10:模型上下文协议(MCP)
2.2.1、示例
package com.penngo.agents.agent.b;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.penngo.agents.test.ApiKeys;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpBlobResourceContents;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.McpException;
import dev.langchain4j.mcp.client.McpGetPromptResult;
import dev.langchain4j.mcp.client.McpPrompt;
import dev.langchain4j.mcp.client.McpPromptContent;
import dev.langchain4j.mcp.client.McpPromptMessage;
import dev.langchain4j.mcp.client.McpReadResourceResult;
import dev.langchain4j.mcp.client.McpResource;
import dev.langchain4j.mcp.client.McpResourceContents;
import dev.langchain4j.mcp.client.McpTextContent;
import dev.langchain4j.mcp.client.McpTextResourceContents;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import dev.langchain4j.mcp.resourcesastools.DefaultMcpResourcesAsToolsPresenter;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static java.time.Duration.ofSeconds;

/**
 * 模式 10:模型上下文协议(MCP / Model Context Protocol)
 * <p>
 * MCP 用客户端-服务器架构把外部能力标准化暴露给 LLM 应用:
 * <pre>
 * LLM 应用(LangChain4j Client) ←─ MCP 协议 ─→ GitHub / DB / 文件 / 企业系统 Server
 * </pre>
 * <p>
 * 本示例演示 LangChain4j 作为 MCP Client 的三类集成方式:
 * <ul>
 *   <li>Tools:通过 {@link McpToolProvider} 把 MCP Server 暴露的工具接入 {@link AiServices},让模型通过 Function Calling 调用;</li>
 *   <li>Resources:通过 {@link McpClient#listResources()} / {@link McpClient#readResource(String)} 读取 MCP 资源;也可用 {@link DefaultMcpResourcesAsToolsPresenter} 把资源访问包装成工具交给模型自行调用;</li>
 *   <li>Prompts:通过 {@link McpClient#listPrompts()} / {@link McpClient#getPrompt(String, Map)} 读取服务端提示模板,用于复用标准工作流。</li>
 * </ul>
 * <p>
 * 运行前准备一个 MCP Server。示例默认使用官方 filesystem server:
 * <pre>
 * npx -y @modelcontextprotocol/server-filesystem D:\\project\\claude\\java\\PAgents
 * </pre>
 * Java 代码会通过 stdio 自动启动该命令,因此需要本机已安装 Node.js / npx。
 * Windows 下如果 Java 找不到 npx,可设置环境变量 MCP_NPX_COMMAND 指向 npx.cmd 的绝对路径,
 * 例如 C:\\Program Files\\nodejs\\npx.cmd。
 */
public class McpAgent {

    /** 对外暴露的智能体接口。toolProvider 会把 MCP tools 动态注入模型可用工具列表。 */
    public interface McpAssistant {
        @SystemMessage("你是一个 MCP 客户端智能体。" +
                "当用户需要读取文件、列目录、搜索资料或调用外部系统能力时,优先使用 MCP Server 暴露的工具。" +
                "如果 MCP 资源工具可用,也可以先列出资源再读取指定资源。" +
                "工具调用完成后,用中文给出简洁回答,并说明你使用了哪个 MCP 能力。")
        @UserMessage("{{question}}")
        String chat(@V("question") String question);
    }

    private final McpClient mcpClient;
    private final McpAssistant assistant;

    public McpAgent(ChatModel model, McpClient mcpClient) {
        this.mcpClient = mcpClient;

        // McpToolProvider 会把 MCP Server 的 tools 转换为 LangChain4j ToolProvider。
        // 注意:Resources / Prompts 是 MCP 可选能力,并不是每个 Server 都支持。
        // filesystem server 主要通过 tools 暴露文件能力;不要强行把 resources 包装成 tools,避免不支持 resources/list 时抛 -32601。
        McpToolProvider toolProvider = McpToolProvider.builder()
                .mcpClients(mcpClient)
                .build();

        this.assistant = AiServices.builder(McpAssistant.class)
                .chatModel(model)
                .toolProvider(toolProvider)
                .build();
    }

    public String chat(String question) {
        System.out.println("\n[User] " + question);
        String answer = assistant.chat(question);
        System.out.println("[Assistant] " + answer);
        return answer;
    }

    /** 列出 MCP Server 暴露的 Tools / Resources / Prompts,帮助理解 API 契约。 */
    public void printServerContract() {
        System.out.println("\n=== MCP Server API 契约 ===");

        System.out.println("\n--- Tools(功能)---");
        for (ToolSpecification tool : mcpClient.listTools()) {
            System.out.println("- " + tool.name() + ":" + tool.description());
            System.out.println("  参数 Schema:" + tool.parameters());
        }

        System.out.println("\n--- Resources(数据)---");
        List<McpResource> resources = safeListResources();
        if (resources.isEmpty()) {
            System.out.println("当前 MCP Server 未暴露 Resources,或不支持 resources/list(这是合法的可选能力)。");
        } else {
            for (McpResource resource : resources) {
                System.out.println("- " + resource.name() + " | uri=" + resource.uri()
                        + " | mime=" + resource.mimeType() + " | " + resource.description());
            }
        }

        System.out.println("\n--- Prompts(模板)---");
        List<McpPrompt> prompts = safeListPrompts();
        if (prompts.isEmpty()) {
            System.out.println("当前 MCP Server 未暴露 Prompts,或不支持 prompts/list(这是合法的可选能力)。");
        } else {
            for (McpPrompt prompt : prompts) {
                System.out.println("- " + prompt.name() + ":" + prompt.description()
                        + " | args=" + prompt.arguments());
            }
        }
    }

    /** 安全列出 Resources。MCP Resources 是可选能力;不支持时返回空列表而不是中断程序。 */
    public List<McpResource> safeListResources() {
        try {
            return mcpClient.listResources();
        } catch (McpException e) {
            if (e.errorCode() == -32601) {
                return List.of();
            }
            throw e;
        }
    }

    /** 安全列出 Prompts。MCP Prompts 是可选能力;不支持时返回空列表而不是中断程序。 */
    public List<McpPrompt> safeListPrompts() {
        try {
            return mcpClient.listPrompts();
        } catch (McpException e) {
            if (e.errorCode() == -32601) {
                return List.of();
            }
            throw e;
        }
    }

    /** 直接读取 Resource,不经过 LLM;适合确定性地把标准上下文注入后续用户消息。 */
    public String readResourceDirectly(String uri) {
        McpReadResourceResult result = mcpClient.readResource(uri);
        return resourceToText(result);
    }

    /** 直接读取 Prompt,不经过 LLM;适合把 MCP Server 维护的标准提示模板复用到业务流程中。 */
    public String getPromptDirectly(String name, Map<String, Object> args) {
        McpGetPromptResult result = mcpClient.getPrompt(name, args == null ? Map.of() : args);
        StringBuilder sb = new StringBuilder();
        if (result.description() != null && !result.description().isBlank()) {
            sb.append("Prompt 描述:").append(result.description()).append("\n");
        }
        for (McpPromptMessage message : result.messages()) {
            sb.append("[").append(message.role()).append("] ")
                    .append(promptContentToText(message.content())).append("\n");
        }
        return sb.toString();
    }

    /** 构造一个 stdio MCP Client:Java 进程启动外部 MCP Server,并通过 stdin/stdout 交换 JSON-RPC 消息。 */
    public static McpClient createFileSystemMcpClient(String rootDirectory) {
        List<String> command = fileSystemServerCommand(rootDirectory);
        System.out.println("MCP 启动命令:" + command);
        StdioMcpTransport transport = StdioMcpTransport.builder()
                .command(command)
                .logEvents(false)
                .build();

        return DefaultMcpClient.builder()
                .key("filesystem")
                .clientName("PAgents-LangChain4j-MCP-Demo")
                .clientVersion("1.0.0")
                .transport(transport)
                .initializationTimeout(ofSeconds(30))
                .toolExecutionTimeout(ofSeconds(60))
                .resourcesTimeout(ofSeconds(30))
                .promptsTimeout(ofSeconds(30))
                .cacheToolList(true)
                .build();
    }

    /**
     * Windows 下 npx 通常是 npx.cmd,Java 的 ProcessBuilder 不会像 shell 一样自动解析 .cmd。
     * 这里优先使用环境变量 MCP_NPX_COMMAND;未设置时 Windows 走 cmd /c npx,其它系统直接走 npx。
     */
    private static List<String> fileSystemServerCommand(String rootDirectory) {
        String customNpx = System.getenv("MCP_NPX_COMMAND");
        if (customNpx != null && !customNpx.isBlank()) {
            return List.of(customNpx, "-y", "@modelcontextprotocol/server-filesystem", rootDirectory);
        }
        if (System.getProperty("os.name").toLowerCase().contains("win")) {
            return List.of("cmd", "/c", "npx", "-y", "@modelcontextprotocol/server-filesystem", rootDirectory);
        }
        return List.of("npx", "-y", "@modelcontextprotocol/server-filesystem", rootDirectory);
    }

    private static String resourceToText(McpReadResourceResult result) {
        StringBuilder sb = new StringBuilder();
        for (McpResourceContents content : result.contents()) {
            if (content instanceof McpTextResourceContents text) {
                sb.append(text.text()).append("\n");
            } else if (content instanceof McpBlobResourceContents blob) {
                sb.append("[二进制资源 uri=").append(blob.uri())
                        .append(", mime=").append(blob.mimeType()).append("]\n");
            } else {
                sb.append(content).append("\n");
            }
        }
        return sb.toString();
    }

    private static String promptContentToText(McpPromptContent content) {
        if (content instanceof McpTextContent text) {
            return text.text();
        }
        return String.valueOf(content);
    }

    public static void main(String[] args) throws Exception {
//        System.setProperty("MCP_NPX_COMMAND","D:\\ProgramData\\nodejs\\node-v20.20.1-win-x64");

        ChatModel model = OpenAiChatModel.builder()
//                .baseUrl(ApiKeys.API_URL)
//                .apiKey(ApiKeys.OPENAI_API_KEY)
//                .modelName(ApiKeys.MODEL_NAME)
                .baseUrl(ApiKeys.API_URL_VOLCENGINE)
                .apiKey(ApiKeys.OPENAI_API_KEY_VOLCENGINE)
                .modelName(ApiKeys.MODEL_NAME_VOLCENGINE)
                .temperature(0.2)
                .timeout(ofSeconds(120))
                .logRequests(true)
                .logResponses(true)
                .build();

        String repoRoot = "D:\\project\\claude\\java\\PAgents";

        System.out.println("=== MCP 开始:" +
                DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss") + " ===");
        System.out.println("MCP Server:filesystem,根目录:" + repoRoot);

        try (McpClient client = createFileSystemMcpClient(repoRoot)) {
            McpAgent agent = new McpAgent(model, client);

            // 1. API 契约发现:MCP Server 暴露了哪些 Tools / Resources / Prompts。
            agent.printServerContract();

            // 2. 让 LLM 决策并调用 MCP Tools。filesystem server 通常会暴露 read_file、list_directory 等工具。
            agent.chat("请通过 MCP 工具读取 pom.xml,概括这个项目使用了哪些主要依赖。不要凭记忆回答。正好验证一下");

            // 3. Resources / Prompts 的直接调用入口:不同 MCP Server 是否提供取决于其实现。
            List<McpResource> resources = agent.safeListResources();
            if (!resources.isEmpty()) {
                String uri = resources.get(0).uri();
                System.out.println("\n[Direct Resource] 读取第一个资源:" + uri);
                System.out.println(agent.readResourceDirectly(uri));
            } else {
                System.out.println("\n[Direct Resource] 当前 filesystem server 未列出静态 Resources;文件能力主要通过 Tools 暴露。Connection OK。");
            }

            List<McpPrompt> prompts = agent.safeListPrompts();
            if (!prompts.isEmpty()) {
                McpPrompt prompt = prompts.get(0);
                Map<String, Object> promptArgs = new HashMap<>();
                prompt.arguments().forEach(arg -> promptArgs.put(arg.name(), "示例值"));
                System.out.println("\n[Direct Prompt] 获取第一个提示模板:" + prompt.name());
                System.out.println(agent.getPromptDirectly(prompt.name(), promptArgs));
            } else {
                System.out.println("\n[Direct Prompt] 当前 filesystem server 未暴露 Prompts;其他 MCP Server 可在这里复用服务端提示模板。Connection OK。");
            }
        }

        System.out.println("=== MCP 结束:" +
                DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss") + " ===");
    }
}

2.2.2、运行结果
=== MCP 开始:2026-06-28 22:14:35 ===
MCP Server:filesystem,根目录:D:\project\claude\java\PAgents
MCP 启动命令:[D:\ProgramData\nodejs\node-v20.20.1-win-x64\npx.cmd, -y, @modelcontextprotocol/server-filesystem, D:\project\claude\java\PAgents]

=== MCP Server API 契约 ===

--- Tools(功能)---
- read_file:Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, tail=JsonNumberSchema {description = "If provided, returns only the last N lines of the file" }, head=JsonNumberSchema {description = "If provided, returns only the first N lines of the file" }}, required = [path], additionalProperties = null, definitions = {} }
- read_text_file:Read the complete contents of a file from the file system as text. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Use the 'head' parameter to read only the first N lines of a file, or the 'tail' parameter to read only the last N lines of a file. Operates on the file as text regardless of extension. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, tail=JsonNumberSchema {description = "If provided, returns only the last N lines of the file" }, head=JsonNumberSchema {description = "If provided, returns only the first N lines of the file" }}, required = [path], additionalProperties = null, definitions = {} }
- read_media_file:Read an image or audio file. Returns the base64 encoded data and MIME type. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }}, required = [path], additionalProperties = null, definitions = {} }
- read_multiple_files:Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files. Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {paths=JsonArraySchema {description = "Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.", items = JsonStringSchema {description = null } }}, required = [paths], additionalProperties = null, definitions = {} }
- write_file:Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, content=JsonStringSchema {description = null }}, required = [path, content], additionalProperties = null, definitions = {} }
- edit_file:Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Returns a git-style diff showing the changes made. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, edits=JsonArraySchema {description = null, items = JsonObjectSchema {description = null, properties = {oldText=JsonStringSchema {description = "Text to search for - must match exactly" }, newText=JsonStringSchema {description = "Text to replace with" }}, required = [oldText, newText], additionalProperties = null, definitions = {} } }, dryRun=JsonBooleanSchema {description = "Preview changes using git-style diff format" }}, required = [path, edits], additionalProperties = null, definitions = {} }
- create_directory:Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }}, required = [path], additionalProperties = null, definitions = {} }
- list_directory:Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }}, required = [path], additionalProperties = null, definitions = {} }
- list_directory_with_sizes:Get a detailed listing of all files and directories in a specified path, including sizes. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is useful for understanding directory structure and finding specific files within a directory. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, sortBy=JsonEnumSchema {description = "Sort entries by name or size", enumValues = [name, size] }}, required = [path], additionalProperties = null, definitions = {} }
- directory_tree:Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories. Files have no children array, while directories always have a children array (which may be empty). The output is formatted with 2-space indentation for readability. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, excludePatterns=JsonArraySchema {description = null, items = JsonStringSchema {description = null } }}, required = [path], additionalProperties = null, definitions = {} }
- move_file:Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {source=JsonStringSchema {description = null }, destination=JsonStringSchema {description = null }}, required = [source, destination], additionalProperties = null, definitions = {} }
- search_files:Recursively search for files and directories matching a pattern. The patterns should be glob-style patterns that match paths relative to the working directory. Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }, pattern=JsonStringSchema {description = null }, excludePatterns=JsonArraySchema {description = null, items = JsonStringSchema {description = null } }}, required = [path, pattern], additionalProperties = null, definitions = {} }
- get_file_info:Retrieve detailed metadata about a file or directory. Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories.
  参数 Schema:JsonObjectSchema {description = null, properties = {path=JsonStringSchema {description = null }}, required = [path], additionalProperties = null, definitions = {} }
- list_allowed_directories:Returns the list of directories that this server is allowed to access. Subdirectories within these allowed directories are also accessible. Use this to understand which directories and their nested paths are available before trying to access files.
  参数 Schema:JsonObjectSchema {description = null, properties = {}, required = [], additionalProperties = null, definitions = {} }

--- Resources(数据)---
当前 MCP Server 未暴露 Resources,或不支持 resources/list(这是合法的可选能力)。

--- Prompts(模板)---
当前 MCP Server 未暴露 Prompts,或不支持 prompts/list(这是合法的可选能力)。

[User] 请通过 MCP 工具读取 pom.xml,概括这个项目使用了哪些主要依赖。不要凭记忆回答。正好验证一下
[Assistant] 已通过 MCP 工具读取 `pom.xml`。这个 Maven 项目的主要信息和依赖如下:

- Java 版本:`21`
- 项目坐标:
  - `groupId`: `com.penngo.agents`
  - `artifactId`: `PAgents`
  - `version`: `1.0.0`

主要依赖:

1. **Hutool**
   - `cn.hutool:hutool-all`
   - 版本:`5.8.42`
   - 常用 Java 工具库

2. **LangChain4j 核心相关**
   - `dev.langchain4j:langchain4j`
   - `dev.langchain4j:langchain4j-core`
   - 版本:`1.15.1`

3. **LangChain4j OpenAI 集成**
   - `dev.langchain4j:langchain4j-open-ai`
   - 版本:`1.15.1`

4. **LangChain4j Skills**
   - `dev.langchain4j:langchain4j-skills`
   - 版本:`1.15.1-beta25`

5. **LangChain4j MCP**
   - `dev.langchain4j:langchain4j-mcp`
   - 版本:`1.15.1-beta25`

另外配置了阿里云 Maven 仓库:

- `https://maven.aliyun.com/repository/public/`

本次使用的 MCP 能力:`list_allowed_directories`、`search_files`、`read_text_file`。

[Direct Resource] 当前 filesystem server 未列出静态 Resources;文件能力主要通过 Tools 暴露。Connection OK。

[Direct Prompt] 当前 filesystem server 未暴露 Prompts;其他 MCP Server 可在这里复用服务端提示模板。Connection OK。
=== MCP 结束:2026-06-28 22:14:45 ===
2.3、模式 14:知识检索(RAG)
2.3.1、示例
package com.penngo.agents.agent.b;

import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.penngo.agents.test.ApiKeys;
import dev.langchain4j.data.document.Metadata;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.output.Response;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.time.Duration.ofSeconds;

/**
 * 模式 14:知识检索(RAG / Retrieval-Augmented Generation)
 * <p>
 * RAG = 检索(Retrieval)+ 增强(Augmented)+ 生成(Generation):
 * <pre>
 * 用户问题 → [检索] → 相关文档片段 → [拼接进提示] → [LLM 生成] → 答案 + 引用
 * </pre>
 * <p>
 * 本示例用纯内存实现一个企业知识助手,覆盖 RAG 的完整链路:
 * <ul>
 *   <li>文档分块(Chunking):把 FAQ / 架构说明拆成较小片段,每片带 source / chunkId 元数据;</li>
 *   <li>嵌入 + 文本相似度:用 {@link HashingEmbeddingModel} 生成向量,存入 {@link InMemoryEmbeddingStore};</li>
 *   <li>检索:按 query embedding 从向量库取 topK 片段;</li>
 *   <li>增强:把片段拼为带引用编号的上下文;</li>
 *   <li>生成:LLM 只能基于引用上下文回答,并输出引用编号;</li>
 *   <li>Agentic RAG:先让 LLM 改写 query,检索后评估证据是否足够,不足则二次检索。</li>
 * </ul>
 * <p>
 * 生产项目中可把 {@link InMemoryEmbeddingStore} 替换为 Milvus / Qdrant / Chroma 等向量数据库,
 * 把 {@link HashingEmbeddingModel} 替换为供应商 embedding 模型。
 */
public class RagAgent {

    /** 检索命中的片段,带可追溯来源。 */
    public record RetrievedChunk(int refId, String source, int chunkId, double score, String text) {}

    /** Agentic RAG 第一步:把用户问题改写为更适合检索的 query。 */
    public record SearchPlan(List<String> queries, String reason) {}

    /** Agentic RAG 中的证据评估结果。 */
    public record EvidenceCheck(boolean enough, List<String> missingInfo, String reason) {}

    /** Query 改写器:主动决定检索关键词,而不是被动把用户原话丢给向量库。 */
    public interface QueryPlanner {
        @SystemMessage("你是 Agentic RAG 的查询规划器。根据用户问题生成 1~3 条适合知识库检索的中文 query。" +
                "query 应包含关键实体、同义词和可能的文档术语。只返回 JSON,字段:queries(字符串数组)、reason(规划理由)。禁止额外文字。")
        @UserMessage("用户问题:{{question}}")
        SearchPlan plan(@V("question") String question);
    }

    /** 证据评估器:判断检索片段是否足以回答问题,不足时给出缺口。 */
    public interface EvidenceJudge {
        @SystemMessage("你是 RAG 证据评估器。判断给定检索片段是否足以回答用户问题。" +
                "只返回 JSON,字段:enough(boolean)、missingInfo(仍缺失的信息数组)、reason(一句话理由)。禁止额外文字。")
        @UserMessage("用户问题:{{question}}\n\n检索片段:\n{{context}}")
        EvidenceCheck check(@V("question") String question, @V("context") String context);
    }

    /** 最终回答生成器:只能基于上下文,必须带引用。 */
    public interface AnswerGenerator {
        @SystemMessage("你是企业知识库问答助手。只能基于用户提供的『知识库片段』回答。" +
                "如果片段不足以回答,要明确说不知道,并说明缺少哪些信息。" +
                "回答中每个关键结论都必须带引用编号,如 [1]、[2]。不要编造未在片段中出现的信息。")
        @UserMessage("用户问题:{{question}}\n\n知识库片段:\n{{context}}\n\n请输出答案,并在末尾列出引用来源。")
        String answer(@V("question") String question, @V("context") String context);
    }

    private final ChatModel chatModel;
    private final EmbeddingModel embeddingModel;
    private final EmbeddingStore<TextSegment> embeddingStore;
    private final QueryPlanner queryPlanner;
    private final EvidenceJudge evidenceJudge;
    private final AnswerGenerator answerGenerator;

    public RagAgent(ChatModel chatModel) {
        this.chatModel = chatModel;
        this.embeddingModel = new HashingEmbeddingModel();
        this.embeddingStore = new InMemoryEmbeddingStore<>();
        this.queryPlanner = AiServices.create(QueryPlanner.class, chatModel);
        this.evidenceJudge = AiServices.create(EvidenceJudge.class, chatModel);
        this.answerGenerator = AiServices.create(AnswerGenerator.class, chatModel);
    }

    /** 构建知识库:分块 → embedding → 入库。 */
    public void ingest(List<KnowledgeDoc> docs) {
        List<TextSegment> segments = new ArrayList<>();
        for (KnowledgeDoc doc : docs) {
            List<String> chunks = chunk(doc.text(), 180, 40);
            for (int i = 0; i < chunks.size(); i++) {
                Metadata metadata = Metadata.from(Map.of(
                        "source", doc.source(),
                        "chunkId", i + 1
                ));
                segments.add(TextSegment.from(chunks.get(i), metadata));
            }
        }
        List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
        embeddingStore.addAll(embeddings, segments);
        System.out.println("[Ingest] 文档数=" + docs.size() + ",分块数=" + segments.size());
    }

    /** 普通 RAG:直接用用户问题检索,然后增强生成。 */
    public RagAnswer answer(String question) {
        System.out.println("\n[RAG] 用户问题:" + question);
        List<RetrievedChunk> chunks = retrieve(List.of(question), 4);
        String context = buildContext(chunks);
        String answer = answerGenerator.answer(question, context);
        return new RagAnswer(answer, chunks);
    }

    /** Agentic RAG:query 改写 → 多 query 检索 → 证据评估 → 必要时二次检索 → 生成。 */
    public RagAnswer agenticAnswer(String question) {
        System.out.println("\n[Agentic RAG] 用户问题:" + question);

        SearchPlan plan = queryPlanner.plan(question);
        System.out.println("[QueryPlanner] " + JSONUtil.toJsonStr(plan));

        List<RetrievedChunk> chunks = retrieve(plan.queries(), 5);
        String context = buildContext(chunks);

        EvidenceCheck check = evidenceJudge.check(question, context);
        System.out.println("[EvidenceJudge] " + JSONUtil.toJsonStr(check));

        if (!check.enough() && !check.missingInfo().isEmpty()) {
            System.out.println("[Agentic RAG] 证据不足,基于缺口二次检索:" + check.missingInfo());
            List<RetrievedChunk> second = retrieve(check.missingInfo(), 3);
            chunks = mergeBySourceAndText(chunks, second).stream().limit(6).toList();
            context = buildContext(chunks);
        }

        String answer = answerGenerator.answer(question, context);
        return new RagAnswer(answer, chunks);
    }

    /** 向量检索,多 query 结果去重并按分数排序。 */
    private List<RetrievedChunk> retrieve(List<String> queries, int topK) {
        Map<String, RetrievedChunk> merged = new LinkedHashMap<>();
        for (String query : queries) {
            Embedding queryEmbedding = embeddingModel.embed(query).content();
            EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
                    .queryEmbedding(queryEmbedding)
                    .maxResults(topK)
                    .minScore(0.05)
                    .build();
            EmbeddingSearchResult<TextSegment> result = embeddingStore.search(request);
            for (EmbeddingMatch<TextSegment> match : result.matches()) {
                TextSegment segment = match.embedded();
                String key = segment.metadata().getString("source") + "#" + segment.metadata().getInteger("chunkId");
                RetrievedChunk old = merged.get(key);
                RetrievedChunk now = new RetrievedChunk(
                        0,
                        segment.metadata().getString("source"),
                        segment.metadata().getInteger("chunkId"),
                        match.score(),
                        segment.text()
                );
                if (old == null || now.score() > old.score()) {
                    merged.put(key, now);
                }
            }
        }
        List<RetrievedChunk> sorted = merged.values().stream()
                .sorted(Comparator.comparingDouble(RetrievedChunk::score).reversed())
                .limit(topK)
                .toList();
        List<RetrievedChunk> withRef = new ArrayList<>();
        for (int i = 0; i < sorted.size(); i++) {
            RetrievedChunk c = sorted.get(i);
            withRef.add(new RetrievedChunk(i + 1, c.source(), c.chunkId(), c.score(), c.text()));
        }
        System.out.println("[Retrieve] query=" + queries + ",命中=" + withRef.size());
        withRef.forEach(c -> System.out.println("  [" + c.refId() + "] score=" + String.format(Locale.ROOT, "%.3f", c.score())
                + " source=" + c.source() + "#" + c.chunkId()));
        return withRef;
    }

    /** 把检索片段拼接成带引用编号的上下文,喂给 LLM。 */
    private static String buildContext(List<RetrievedChunk> chunks) {
        StringBuilder sb = new StringBuilder();
        for (RetrievedChunk c : chunks) {
            sb.append("[").append(c.refId()).append("] ")
                    .append("source=").append(c.source()).append("#chunk").append(c.chunkId())
                    .append(" score=").append(String.format(Locale.ROOT, "%.3f", c.score()))
                    .append("\n")
                    .append(c.text()).append("\n\n");
        }
        return sb.isEmpty() ? "(未检索到相关片段)" : sb.toString();
    }

    /** 简单字符分块,保留 overlap,模拟生产 RAG 中的 chunking。 */
    private static List<String> chunk(String text, int chunkSize, int overlap) {
        List<String> chunks = new ArrayList<>();
        String normalized = text.replaceAll("\\s+", " ").trim();
        int start = 0;
        while (start < normalized.length()) {
            int end = Math.min(normalized.length(), start + chunkSize);
            chunks.add(normalized.substring(start, end));
            if (end == normalized.length()) break;
            start = Math.max(0, end - overlap);
        }
        return chunks;
    }

    private static List<RetrievedChunk> mergeBySourceAndText(List<RetrievedChunk> a, List<RetrievedChunk> b) {
        Map<String, RetrievedChunk> map = new LinkedHashMap<>();
        for (RetrievedChunk c : a) map.put(c.source() + c.chunkId(), c);
        for (RetrievedChunk c : b) map.putIfAbsent(c.source() + c.chunkId(), c);
        return new ArrayList<>(map.values());
    }

    /** 本地轻量 Hashing Embedding:用于示例,不依赖外部 embedding API。 */
    public static class HashingEmbeddingModel implements EmbeddingModel {
        private static final int DIM = 256;
        private static final Pattern TOKEN = Pattern.compile("[a-zA-Z0-9_\\-]+|[\\u4e00-\\u9fa5]");

        @Override
        public Response<List<Embedding>> embedAll(List<TextSegment> segments) {
            return Response.from(segments.stream().map(s -> embedText(s.text())).toList());
        }

        @Override
        public int dimension() {
            return DIM;
        }

        @Override
        public String modelName() {
            return "local-hashing-embedding-demo";
        }

        private static Embedding embedText(String text) {
            float[] vector = new float[DIM];
            Matcher matcher = TOKEN.matcher(text.toLowerCase(Locale.ROOT));
            while (matcher.find()) {
                String token = matcher.group();
                int h = token.hashCode();
                int index = Math.floorMod(h, DIM);
                float sign = (h & 1) == 0 ? 1f : -1f;
                vector[index] += sign;
            }
            Embedding embedding = Embedding.from(vector);
            embedding.normalize();
            return embedding;
        }
    }

    public record KnowledgeDoc(String source, String text) {}
    public record RagAnswer(String answer, List<RetrievedChunk> references) {}

    /** 示例知识库:模拟企业私有文档 / FAQ。 */
    private static List<KnowledgeDoc> demoKnowledgeBase() {
        return List.of(
                new KnowledgeDoc("客服大模型接入FAQ.md", """
                        企业客服系统接入大模型时,应优先从只读问答场景开始,例如 FAQ 回答、工单摘要、相似工单推荐。
                        首期不建议直接自动退款、自动改订单状态等高风险动作。上线前需要准备人工兜底、置信度阈值、敏感信息脱敏和操作日志。
                        如果模型答案来自知识库,应返回引用来源,便于客服复核。
                        """),
                new KnowledgeDoc("RAG架构设计.md", """
                        RAG 系统通常包含文档解析、分块、Embedding、向量检索、上下文拼接和生成六个阶段。
                        文档分块建议控制在 200 到 800 token 之间,并保留 10% 到 20% 的 overlap,避免关键信息被切断。
                        向量数据库可以选择 Milvus、Qdrant、Chroma,也可以在小规模场景使用内存向量库。
                        """),
                new KnowledgeDoc("安全合规要求.md", """
                        企业知识助手必须避免泄露个人信息、合同金额、内部密钥等敏感数据。
                        对客服工单中的手机号、身份证号、地址应做脱敏处理。
                        对会触发现实动作的工具,例如发邮件、下单、退款、删除数据,应设置人工确认或审批流程。
                        """),
                new KnowledgeDoc("AgenticRAG实践.md", """
                        Agentic RAG 不只是被动检索。Agent 可以先改写 query,选择多个数据源,检索后评估证据是否足够。
                        如果证据不足,Agent 应识别缺口并再次检索;如果不同来源冲突,应明确说明冲突而不是强行合并。
                        这种方式适合研究助手、企业知识助手和复杂客服问题。
                        """),
                new KnowledgeDoc("项目README.md", """
                        PAgents 是一个 Java 21 的 AI Agent 学习项目,使用 LangChain4j 和 Hutool AI 对接 DeepSeek、通义千问、火山引擎等模型。
                        示例类通常包含 main 方法,可在 IDEA 中直接运行。API Key 集中在 ApiKeys 中读取环境变量。
                        """)
        );
    }

    public static void main(String[] args) {
        ChatModel model = OpenAiChatModel.builder()
//                .baseUrl(ApiKeys.API_URL)
//                .apiKey(ApiKeys.OPENAI_API_KEY)
//                .modelName(ApiKeys.MODEL_NAME)
                .baseUrl(ApiKeys.API_URL_VOLCENGINE)
                .apiKey(ApiKeys.OPENAI_API_KEY_VOLCENGINE)
                .modelName(ApiKeys.MODEL_NAME_VOLCENGINE)
                .temperature(0.2)
                .timeout(ofSeconds(120))
                .logRequests(true)
                .logResponses(true)
                .build();

        RagAgent agent = new RagAgent(model);

        System.out.println("=== RAG 开始:" + DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss") + " ===");
        agent.ingest(demoKnowledgeBase());

        String question = "我们要做企业客服知识助手,第一期应该怎么上线,哪些动作需要人工确认?";

        RagAnswer normal = agent.answer(question);
        System.out.println("\n--- 普通 RAG 答案 ---\n" + normal.answer());
        System.out.println("引用:" + JSONUtil.toJsonStr(normal.references()));

        RagAnswer agentic = agent.agenticAnswer("Agentic RAG 相比普通 RAG 有什么改进?如果证据不足该怎么办?");
        System.out.println("\n--- Agentic RAG 答案 ---\n" + agentic.answer());
        System.out.println("引用:" + JSONUtil.toJsonStr(agentic.references()));

        System.out.println("=== RAG 结束:" + DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss") + " ===");
    }
}

2.3.2、运行结果
=== RAG 开始:2026-06-28 22:19:27 ===
[Ingest] 文档数=5,分块数=5

[RAG] 用户问题:我们要做企业客服知识助手,第一期应该怎么上线,哪些动作需要人工确认?
[Retrieve] query=[我们要做企业客服知识助手,第一期应该怎么上线,哪些动作需要人工确认?],命中=4
  [1] score=0.794 source=客服大模型接入FAQ.md#1
  [2] score=0.743 source=安全合规要求.md#1
  [3] score=0.714 source=AgenticRAG实践.md#1
  [4] score=0.598 source=RAG架构设计.md#1

--- 普通 RAG 答案 ---
建议第一期按“低风险、只读、可复核”的方式上线企业客服知识助手。具体如下:

## 一、第一期上线范围

1. **优先上线只读问答场景**:例如 FAQ 回答、客服工单摘要、相似工单推荐,不直接执行会改变业务状态的动作。[1]  
2. **暂不做高风险自动化操作**:首期不建议让模型自动退款、自动修改订单状态等,因为这些属于高风险动作。[1]  
3. **答案必须可追溯**:如果模型答案来自知识库,应返回引用来源,方便客服人员复核。[1]  
4. **上线前必须准备人工兜底机制**:当模型置信度不足、证据不足或问题复杂时,应转人工处理。[1][3]  
5. **设置置信度阈值**:低于阈值时不要直接给确定答案,应进入人工兜底或补充检索流程。[1][3]  
6. **做好敏感信息脱敏**:客服工单中的手机号、身份证号、地址等个人信息需要脱敏,避免泄露敏感数据。[2]  
7. **保留操作日志**:上线前需要准备操作日志,便于后续审计和问题追踪。[1]  

## 二、建议的第一期系统能力

1. **知识库问答**:基于企业 FAQ、客服 SOP、产品说明等资料回答客服问题,并附带来源引用。[1]  
2. **工单摘要**:对历史工单或当前会话做摘要,辅助客服快速理解问题背景。[1]  
3. **相似工单推荐**:根据当前问题推荐历史相似工单,帮助客服参考已有处理方式。[1]  
4. **证据不足时不强答**:如果检索到的证据不足,助手应识别信息缺口并再次检索;仍不足时应说明无法确认,而不是编造答案。[3]  
5. **多来源冲突时明确提示**:如果不同知识来源之间存在冲突,应明确说明冲突,而不是强行合并成一个确定结论。[3]  

## 三、哪些动作需要人工确认或审批

以下动作都应设置人工确认或审批流程:

1. **退款**:自动退款属于高风险动作,首期不建议直接自动执行,应人工确认。[1][2]  
2. **修改订单状态**:例如自动改订单状态,首期不建议由模型直接执行,应人工确认。[1]  
3. **发邮件**:发邮件会触发现实动作,应设置人工确认或审批流程。[2]  
4. **下单**:下单属于会触发现实动作的工具调用,应人工确认或审批。[2]  
5. **删除数据**:删除数据风险较高,应设置人工确认或审批流程。[2]  
6. **任何会触发现实业务动作的工具调用**:凡是会影响客户、订单、资金、数据或外部系统状态的动作,都应人工确认或审批。[2]  

## 四、推荐上线策略

1. **第一阶段:客服辅助,不自动执行操作**  
   助手只提供答案、摘要、推荐和引用来源,由客服人员最终判断和执行。[1]

2. **第二阶段:加置信度和人工兜底**  
   对高置信度问题给出参考答案;对低置信度、证据不足或来源冲突的问题转人工。[1][3]

3. **第三阶段:谨慎接入工具能力**  
   如果
引用:[{"refId":1,"source":"客服大模型接入FAQ.md","chunkId":1,"score":0.7937396246914611,"text":"企业客服系统接入大模型时,应优先从只读问答场景开始,例如 FAQ 回答、工单摘要、相似工单推荐。 首期不建议直接自动退款、自动改订单状态等高风险动作。上线前需要准备人工兜底、置信度阈值、敏感信息脱敏和操作日志。 如果模型答案来自知识库,应返回引用来源,便于客服复核。"},{"refId":2,"source":"安全合规要求.md","chunkId":1,"score":0.7433157576407362,"text":"企业知识助手必须避免泄露个人信息、合同金额、内部密钥等敏感数据。 对客服工单中的手机号、身份证号、地址应做脱敏处理。 对会触发现实动作的工具,例如发邮件、下单、退款、删除数据,应设置人工确认或审批流程。"},{"refId":3,"source":"AgenticRAG实践.md","chunkId":1,"score":0.714272780509963,"text":"Agentic RAG 不只是被动检索。Agent 可以先改写 query,选择多个数据源,检索后评估证据是否足够。 如果证据不足,Agent 应识别缺口并再次检索;如果不同来源冲突,应明确说明冲突而不是强行合并。 这种方式适合研究助手、企业知识助手和复杂客服问题。"},{"refId":4,"source":"RAG架构设计.md","chunkId":1,"score":0.5976086003099519,"text":"RAG 系统通常包含文档解析、分块、Embedding、向量检索、上下文拼接和生成六个阶段。 文档分块建议控制在 200 到 800 token 之间,并保留 10% 到 20% 的 overlap,避免关键信息被切断。 向量数据库可以选择 Milvus、Qdrant、Chroma,也可以在小规模场景使用内存向量库。"}]

[Agentic RAG] 用户问题:Agentic RAG 相比普通 RAG 有什么改进?如果证据不足该怎么办?
[QueryPlanner] {"queries":["Agentic RAG 相比 普通 RAG 传统 RAG 的改进 优势 自主规划 工具调用 多步检索 反思 迭代检索","Agentic RAG 工作流程 query planning 查询规划 evidence validation 证据验证 self-reflection 自我反思 answer synthesis 答案生成","RAG 证据不足 怎么办 insufficient evidence 低置信度 拒答 澄清问题 补充检索 引用来源"],"reason":"用户问题包含两部分:比较 Agentic RAG 与普通/传统 RAG 的改进,以及在证据不足时的处理策略。因此检索 query 覆盖核心概念、同义术语和常见文档术语,如自主规划、多步/迭代检索、工具调用、自我反思、证据验证、低置信度、拒答和澄清问题。"}
[Retrieve] query=[Agentic RAG 相比 普通 RAG 传统 RAG 的改进 优势 自主规划 工具调用 多步检索 反思 迭代检索, Agentic RAG 工作流程 query planning 查询规划 evidence validation 证据验证 self-reflection 自我反思 answer synthesis 答案生成, RAG 证据不足 怎么办 insufficient evidence 低置信度 拒答 澄清问题 补充检索 引用来源],命中=5
  [1] score=0.730 source=AgenticRAG实践.md#1
  [2] score=0.676 source=安全合规要求.md#1
  [3] score=0.672 source=客服大模型接入FAQ.md#1
  [4] score=0.645 source=RAG架构设计.md#1
  [5] score=0.611 source=项目README.md#1
[EvidenceJudge] {"enough":true,"missingInfo":[],"reason":"片段已说明普通 RAG 的基本流程,并明确给出 Agentic RAG 的改进点以及证据不足时应识别缺口并再次检索的处理方式。"}

--- Agentic RAG 答案 ---
Agentic RAG 相比普通 RAG 的主要改进是:普通 RAG 通常按“文档解析、分块、Embedding、向量检索、上下文拼接、生成”的固定流程运行 [4];而 Agentic RAG 不只是被动检索,它可以先改写 query、选择多个数据源,并在检索后评估证据是否足够 [1]。  

如果证据不足,Agent 应识别当前证据缺口并再次检索 [1]。如果不同来源之间存在冲突,应明确说明冲突,而不是强行合并成一个看似一致的答案 [1]。  

引用来源:  
[1] AgenticRAG实践.md#chunk1  
[4] RAG架构设计.md#chunk1
引用:[{"refId":1,"source":"AgenticRAG实践.md","chunkId":1,"score":0.7301023487849997,"text":"Agentic RAG 不只是被动检索。Agent 可以先改写 query,选择多个数据源,检索后评估证据是否足够。 如果证据不足,Agent 应识别缺口并再次检索;如果不同来源冲突,应明确说明冲突而不是强行合并。 这种方式适合研究助手、企业知识助手和复杂客服问题。"},{"refId":2,"source":"安全合规要求.md","chunkId":1,"score":0.6759198030407042,"text":"企业知识助手必须避免泄露个人信息、合同金额、内部密钥等敏感数据。 对客服工单中的手机号、身份证号、地址应做脱敏处理。 对会触发现实动作的工具,例如发邮件、下单、退款、删除数据,应设置人工确认或审批流程。"},{"refId":3,"source":"客服大模型接入FAQ.md","chunkId":1,"score":0.6720311756511397,"text":"企业客服系统接入大模型时,应优先从只读问答场景开始,例如 FAQ 回答、工单摘要、相似工单推荐。 首期不建议直接自动退款、自动改订单状态等高风险动作。上线前需要准备人工兜底、置信度阈值、敏感信息脱敏和操作日志。 如果模型答案来自知识库,应返回引用来源,便于客服复核。"},{"refId":4,"source":"RAG架构设计.md","chunkId":1,"score":0.6450647150070266,"text":"RAG 系统通常包含文档解析、分块、Embedding、向量检索、上下文拼接和生成六个阶段。 文档分块建议控制在 200 到 800 token 之间,并保留 10% 到 20% 的 overlap,避免关键信息被切断。 向量数据库可以选择 Milvus、Qdrant、Chroma,也可以在小规模场景使用内存向量库。"},{"refId":5,"source":"项目README.md","chunkId":1,"score":0.6105672498725694,"text":"PAgents 是一个 Java 21 的 AI Agent 学习项目,使用 LangChain4j 和 Hutool AI 对接 DeepSeek、通义千问、火山引擎等模型。 示例类通常包含 main 方法,可在 IDEA 中直接运行。API Key 集中在 ApiKeys 中读取环境变量。"}]
=== RAG 结束:2026-06-28 22:19:55 ===
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

penngo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值