掘金 人工智能 14小时前
joyagent智能体学习(第3期)工具系统设计与实现
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入剖析了JoyAgent-JDGenie项目中的工具系统架构,将其比作智能体的“双手”,是连接LLM思维与现实世界的关键桥梁。文章从统一接口、分层实现、插件化扩展的设计理念出发,详细介绍了BaseTool接口、ToolCollection调度中心,以及CodeInterpreterTool等核心组件的Java和Python端实现。该系统通过支持内置工具与第三方MCP工具集成,实现了强大的功能扩展和高效的交互能力,为构建现代多智能体系统提供了坚实的工具体系支撑。

📐 **统一接口与分层实现的设计理念**:JoyAgent-JDGenie的工具系统采用BaseTool接口规范所有工具行为,并通过Java后端进行工具调度,Python服务负责具体执行,实现了统一的接口和分层的实现方式,为插件化扩展奠定了基础。

📐 **核心组件架构与调度中心**:系统以智能体Agent为起点,通过ToolCollection工具集合管理内置工具(如CodeInterpreterTool、DeepSearchTool等)和MCP工具,并由BaseTool接口定义统一的行为契约,实现了工具的注册、发现和统一调度。

🔧 **CodeInterpreterTool的深度解析**:作为核心工具,CodeInterpreterTool能够处理数据处理、数据分析和图表生成等任务。其Java端通过调用Python服务实现,Python端则利用FastAPI提供代码执行服务,并支持流式输出,提升了交互体验。

🔧 **Java端与Python端协同工作**:Java端负责定义工具接口、管理工具集合和发起调用,而Python端则具体执行代码解释等任务。这种分工协作的模式,使得系统能够灵活地集成不同语言的服务,并高效地完成复杂任务。

本章核心:深度剖析JoyAgent-JDGenie项目的工具系统架构,从基础接口设计到具体工具实现,再到MCP生态集成,全面解析现代多智能体系统的工具体系构建思路。

引言:工具系统的战略地位

在多智能体系统中,工具系统扮演着"智能体大脑的双手"角色。如果说LLM是智能体的思维核心,那么工具系统就是智能体与现实世界交互的桥梁。JoyAgent-JDGenie项目构建了一套完整的工具系统架构,包含从抽象接口设计到具体工具实现,从内置工具到第三方工具集成的完整生态。

本章将通过"总-分-总"的结构,首先介绍工具系统的整体架构设计理念,然后深入分析各个核心组件的实现细节,最后总结工具系统的设计精髓和扩展思路。


第一部分:工具系统总体架构 📐

3.1 工具系统设计理念

JoyAgent-JDGenie的工具系统采用了统一接口、分层实现、插件化扩展的设计理念:

3.2 核心组件架构

graph TB    A[智能体Agent] --> B[ToolCollection工具集合]    B --> C[BaseTool接口]    C --> D[内置工具]    C --> E[MCP工具]        D --> F[CodeInterpreterTool]    D --> G[DeepSearchTool]    D --> H[ReportTool]    D --> I[FileTool]    D --> J[PlanningTool]        E --> K[第三方MCP服务]        F --> L[Python工具服务]    G --> L    H --> L    I --> L        L --> M[具体工具实现]    L --> N[FastAPI服务]

3.3 BaseTool接口设计

工具系统的核心是BaseTool接口,它定义了所有工具必须遵循的统一契约:

package com.jd.genie.agent.tool;import java.util.Map;/** * 工具基接口 */public interface BaseTool {    String getName();    String getDescription();    Map<String, Object> toParams();    Object execute(Object input);}

这个简洁而强大的接口设计体现了几个关键思想:

    名称标识getName()提供工具的唯一标识符功能描述getDescription()为LLM提供工具选择的依据参数规范toParams()定义工具的输入参数结构执行逻辑execute()实现具体的工具功能

3.4 ToolCollection工具集合管理

ToolCollection类作为工具系统的调度中心,负责工具的注册、发现和执行:

package com.jd.genie.agent.tool;import com.alibaba.fastjson.JSONObject;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.dto.tool.McpToolInfo;import com.jd.genie.agent.tool.mcp.McpTool;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import java.util.HashMap;import java.util.Map;/** * 工具集合类 - 管理可用的工具 */@Data@Slf4jpublic class ToolCollection {    private Map<String, BaseTool> toolMap;    private Map<String, McpToolInfo> mcpToolMap;    private AgentContext agentContext;    /**     * 数字员工列表     * task未并发的情况下     * 1、每一个task,执行时,数字员工列表就会更新     * TODO 并发情况下需要处理     */    private String currentTask;    private JSONObject digitalEmployees;    public ToolCollection() {        this.toolMap = new HashMap<>();        this.mcpToolMap = new HashMap<>();    }    /**     * 添加工具     */    public void addTool(BaseTool tool) {        toolMap.put(tool.getName(), tool);    }    /**     * 获取工具     */    public BaseTool getTool(String name) {        return toolMap.get(name);    }    /**     * 添加MCP工具     */    public void addMcpTool(String name, String desc, String parameters, String mcpServerUrl) {        mcpToolMap.put(name, McpToolInfo.builder()                .name(name)                .desc(desc)                .parameters(parameters)                .mcpServerUrl(mcpServerUrl)                .build());    }    /**     * 获取MCP工具     */    public McpToolInfo getMcpTool(String name) {        return mcpToolMap.get(name);    }    /**     * 执行工具     */    public Object execute(String name, Object toolInput) {        if (toolMap.containsKey(name)) {            BaseTool tool = getTool(name);            return tool.execute(toolInput);        } else if (mcpToolMap.containsKey(name)) {            McpToolInfo toolInfo = mcpToolMap.get(name);            McpTool mcpTool = new McpTool();            mcpTool.setAgentContext(agentContext);            return mcpTool.callTool(toolInfo.getMcpServerUrl(), name, toolInput);        } else {            log.error("Error: Unknown tool {}", name);        }        return null;    }    /**     * 设置数字员工     */    public void updateDigitalEmployee(JSONObject digitalEmployee) {        if (digitalEmployee == null) {            log.error("requestId:{} setDigitalEmployee: {}", agentContext.getRequestId(), digitalEmployee);        }        setDigitalEmployees(digitalEmployee);    }    /**     * 获取数字员工名称     */    public String getDigitalEmployee(String toolName) {        if (StringUtils.isEmpty(toolName)) {            return null;        }        if (digitalEmployees == null) {            return null;        }        return (String) digitalEmployees.get(toolName);    }}

ToolCollection的设计亮点:

    双重工具管理:同时支持内置工具(toolMap)和MCP工具(mcpToolMap)统一执行接口execute()方法自动路由到相应的工具类型数字员工支持:支持为不同工具分配专门的数字员工进行个性化展示

第二部分:核心工具深度解析 🔧

3.5 代码解释器工具(CodeInterpreterTool)

3.5.1 工具设计理念

代码解释器是JoyAgent-JDGenie最核心的工具之一,它为智能体提供了强大的编程和数据处理能力。通过集成smolagents框架,实现了安全的Python代码执行环境。

3.5.2 Java端实现

package com.jd.genie.agent.tool.common;import com.alibaba.fastjson.JSONObject;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.dto.CodeInterpreterRequest;import com.jd.genie.agent.dto.CodeInterpreterResponse;import com.jd.genie.agent.dto.File;import com.jd.genie.agent.tool.BaseTool;import com.jd.genie.agent.util.SpringContextHolder;import com.jd.genie.config.GenieConfig;import lombok.Data;import lombok.extern.slf4j.Slf4j;import okhttp3.*;import org.springframework.context.ApplicationContext;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.*;import java.util.concurrent.CompletableFuture;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;import java.util.stream.Collectors;@Slf4j@Datapublic class CodeInterpreterTool implements BaseTool {    private AgentContext agentContext;    @Override    public String getName() {        return "code_interpreter";    }    @Override    public String getDescription() {        String desc = "这是一个代码工具,可以通过编写代码完成数据处理、数据分析、图表生成等任务";        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        return genieConfig.getCodeAgentDesc().isEmpty() ? desc : genieConfig.getCodeAgentDesc();    }    @Override    public Map<String, Object> toParams() {        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        if (!genieConfig.getCodeAgentPamras().isEmpty()) {            return genieConfig.getCodeAgentPamras();        }        Map<String, Object> taskParam = new HashMap<>();        taskParam.put("type", "string");        taskParam.put("description", "需要完成的任务以及完成任务需要的数据,需要尽可能详细");        Map<String, Object> parameters = new HashMap<>();        parameters.put("type", "object");        Map<String, Object> properties = new HashMap<>();        properties.put("task", taskParam);        parameters.put("properties", properties);        parameters.put("required", Collections.singletonList("task"));        return parameters;    }    @Override    public Object execute(Object input) {        try {            Map<String, Object> params = (Map<String, Object>) input;            String task = (String) params.get("task");            List<String> fileNames = agentContext.getProductFiles().stream().map(File::getFileName).collect(Collectors.toList());            CodeInterpreterRequest request = CodeInterpreterRequest.builder()                    .requestId(agentContext.getSessionId()) // 适配多轮对话                    .query(agentContext.getQuery())                    .task(task)                    .fileNames(fileNames)                    .stream(true)                    .build();            // 调用流式 API            Future future = callCodeAgentStream(request);            Object object = future.get();            return object;        } catch (Exception e) {            log.error("{} code agent error", agentContext.getRequestId(), e);        }        return null;    }    /**     * 调用 CodeAgent     */    public CompletableFuture<String> callCodeAgentStream(CodeInterpreterRequest codeRequest) {        CompletableFuture<String> future = new CompletableFuture<>();        try {            OkHttpClient client = new OkHttpClient.Builder()                    .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为 60 秒                    .readTimeout(300, TimeUnit.SECONDS)    // 设置读取超时时间为 300 秒                    .writeTimeout(300, TimeUnit.SECONDS)   // 设置写入超时时间为 300 秒                    .callTimeout(300, TimeUnit.SECONDS)    // 设置调用超时时间为 300 秒                    .build();            ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();            GenieConfig genieConfig = applicationContext.getBean(GenieConfig.class);            String url = genieConfig.getCodeInterpreterUrl() + "/v1/tool/code_interpreter";            RequestBody body = RequestBody.create(                    MediaType.parse("application/json"),                    JSONObject.toJSONString(codeRequest)            );            log.info("{} code_interpreter request {}", agentContext.getRequestId(), JSONObject.toJSONString(codeRequest));            Request.Builder requestBuilder = new Request.Builder()                    .url(url)                    .post(body);            Request request = requestBuilder.build();            client.newCall(request).enqueue(new Callback() {                @Override                public void onFailure(Call call, IOException e) {                    log.error("{} code_interpreter on failure", agentContext.getRequestId(), e);                    future.completeExceptionally(e);                }                @Override                public void onResponse(Call call, Response response) {                    log.info("{} code_interpreter response {} {} {}", agentContext.getRequestId(), response, response.code(), response.body());                    CodeInterpreterResponse codeResponse = CodeInterpreterResponse.builder()                            .codeOutput("code_interpreter执行失败") // 默认输出                            .build();                    try (ResponseBody responseBody = response.body()) {                        if (!response.isSuccessful() || responseBody == null) {                            log.error("{} code_interpreter request error", agentContext.getRequestId());                            future.completeExceptionally(new IOException("Unexpected response code: " + response));                            return;                        }                        String line;                        BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody.byteStream()));                        while ((line = reader.readLine()) != null) {                            if (line.startsWith("data: ")) {                                String data = line.substring(6);                                if (data.equals("[DONE]")) {                                    break;                                }                                if (data.startsWith("heartbeat")) {                                    continue;                                }                                log.info("{} code_interpreter recv data: {}", agentContext.getRequestId(), data);                                codeResponse = JSONObject.parseObject(data, CodeInterpreterResponse.class);                                if (Objects.nonNull(codeResponse.getFileInfo()) && !codeResponse.getFileInfo().isEmpty()) {                                    for (CodeInterpreterResponse.FileInfo fileInfo : codeResponse.getFileInfo()) {                                        File file = File.builder()                                                .fileName(fileInfo.getFileName())                                                .ossUrl(fileInfo.getOssUrl())                                                .domainUrl(fileInfo.getDomainUrl())                                                .fileSize(fileInfo.getFileSize())                                                .description(fileInfo.getFileName()) // fileName用作描述                                                .isInternalFile(false)                                                .build();                                        agentContext.getProductFiles().add(file);                                        agentContext.getTaskProductFiles().add(file);                                    }                                }                                String digitalEmployee = agentContext.getToolCollection().getDigitalEmployee(getName());                                log.info("requestId:{} task:{} toolName:{} digitalEmployee:{}", agentContext.getRequestId(),                                        agentContext.getToolCollection().getCurrentTask(), getName(), digitalEmployee);                                agentContext.getPrinter().send("code", codeResponse, digitalEmployee);                            }                        }                    } catch (Exception e) {                        log.error("{} code_interpreter request error", agentContext.getRequestId(), e);                        future.completeExceptionally(e);                        return;                    }                    /**                     * {{输出内容}}                     * \n\n                     * 其中保存了文件:                     * {{文件名}}                     */                    StringBuilder output = new StringBuilder();                    output.append(codeResponse.getCodeOutput());                    if (Objects.nonNull(codeResponse.getFileInfo()) && !codeResponse.getFileInfo().isEmpty()) {                        output.append("\n\n其中保存了文件: ");                        for (CodeInterpreterResponse.FileInfo fileInfo : codeResponse.getFileInfo()) {                            output.append(fileInfo.getFileName()).append("\n");                        }                    }                    future.complete(output.toString());                }            });        } catch (Exception e) {            log.error("{} code_interpreter request error", agentContext.getRequestId(), e);            future.completeExceptionally(e);        }        return future;    }}

3.5.3 Python服务端实现

Python端通过FastAPI提供代码执行服务:

@router.post("/code_interpreter")async def post_code_interpreter(    body: CIRequest,):     # 处理文件路径    if body.file_names:        for idx, f_name in enumerate(body.file_names):            if not f_name.startswith("/") and not f_name.startswith("http"):                body.file_names[idx] = f"{os.getenv('FILE_SERVER_URL')}/preview/{body.request_id}/{f_name}"    async def _stream():        acc_content = ""        acc_token = 0        acc_time = time.time()        async for chunk in code_interpreter_agent(            task=body.task,            file_names=body.file_names,            request_id=body.request_id,            stream=True,        ):            if isinstance(chunk, CodeOuput):                yield ServerSentEvent(                    data=json.dumps(                        {                            "requestId": body.request_id,                            "code": chunk.code,                            "fileInfo": chunk.file_list,                            "isFinal": False,                        },                        ensure_ascii=False,                    )                )            elif isinstance(chunk, ActionOutput):                yield ServerSentEvent(                    data=json.dumps(                        {                            "requestId": body.request_id,                            "codeOutput": chunk.content,                            "fileInfo": chunk.file_list,                            "isFinal": True,                        },                        ensure_ascii=False,                    )                )                yield ServerSentEvent(data="[DONE]")            else:                acc_content += chunk                acc_token += 1                if body.stream_mode.mode == "general":                    yield ServerSentEvent(                        data=json.dumps(                            {"requestId": body.request_id, "data": chunk, "isFinal": False},                            ensure_ascii=False,                        )                    )                elif body.stream_mode.mode == "token":                    if acc_token >= body.stream_mode.token:                        yield ServerSentEvent(                            data=json.dumps(                                {                                    "requestId": body.request_id,                                    "data": acc_content,                                    "isFinal": False,                                },                                ensure_ascii=False,                            )                        )                        acc_token = 0                        acc_content = ""                elif body.stream_mode.mode == "time":                    if time.time() - acc_time > body.stream_mode.time:                        yield ServerSentEvent(                            data=json.dumps(                                {                                    "requestId": body.request_id,                                    "data": acc_content,                                    "isFinal": False,                                },                                ensure_ascii=False,                            )                        )                        acc_time = time.time()                        acc_content = ""                if body.stream_mode.mode in ["time", "token"] and acc_content:                    yield ServerSentEvent(                        data=json.dumps(                            {                                "requestId": body.request_id,                                "data": acc_content,                                "isFinal": False,                            },                            ensure_ascii=False,                        )                    )                if body.stream:        return EventSourceResponse(            _stream(),            ping_message_factory=lambda: ServerSentEvent(data="heartbeat"),            ping=15,        )

3.5.4 提示词工程

代码解释器使用了精心设计的提示词模板:

system_prompt: |-  You are AI assistant who can solve any task using python code. You will be given a task to solve as best you can.  To do so, you have been given access to a list of tools: these tools are basically Python functions which you can call with code.  To solve the task, you must plan forward to proceed in a series of steps, in a cycle of 'Thought:', 'Code:', and 'Observation:' sequences.    At each step, in the 'Task:' sequence, you can give a brief task description.  And in the 'Thought:' sequence, you should first explain your reasoning towards solving the task and the tools that you want to use.  Then in the 'Code:' sequence, you should write the code in simple Python. The code sequence must end with '</code>' sequence.  During each intermediate step, you can use 'print()' to save whatever important information you will then need.  These print outputs will then appear in the 'Observation:' field, which will be available as input for the next step. **Crucially, ensure that only key metric data is printed, and avoid printing file saving descriptions or file paths.**  In the end you have to return a final answer using the `final_answer` tool.    Please follow the format below to solve the given task step by step:    ---  Task: {Task Description. One brief sentence, no punctuation}    Thought: {Your reasoning and planned tools}  Code:  <code>  {Code block}  </code>  Observation: {Observed output from code}    ---    Here are the rules you should always follow to solve your task:    1.  Always provide a 'Task:' sequence and a 'Thought:' sequence followed by a 'Code:' sequence, ending with '</code>'. Failure to do so will result in task failure.  2.  Avoid chaining too many sequential tool calls within a single code block, especially when output formats are unpredictable. For tools like search, which have variable return formats, use 'print()' to pass results between blocks.  3.  Call tools only when necessary, and avoid redundant calls with identical parameters.  4.  Do not use tool names as variable names (e.g., avoid naming a variable 'final_answer').  5.  Do not create notional variables that are not actually used in the code, as this can disrupt the logging of true variables.  6.  Variables and imported modules persist between code executions.  7.  All actions must be performed using Python code; manual processing is prohibited.  8.  NOTICE: **Falsification of data is strictly prohibited**!    # language rules  - Default working language: **Chinese**  - Use the language specified by user in messages as the working language when explicitly provided  - All thinking and responses must be in the working language  - Natural language arguments in tool calls must be in the working language  - Avoid using pure lists and bullet points format in any language  - But **Python Code uses English**.task_template: |-  {% if files %}  你有如下文件可以参考,对于 csv、excel、等数据文件则提供的只是部分数据,如果需要请你读取文件获取全文信息  <docs>    {% for file in files %}    <doc>      <path>{{ file['path'] }}</path>      <abstract>{{ file['abstract'] }}</abstract>    </doc>    {% endfor %}  </docs>  {% endif %}  ## 要求      1. 如果有 excel、csv 文件,使用 pandas 读取、保存,使用 openpyxl 引擎,其他文件类型不要直接读取成 DataFrame;  2. 不要做额外的校验逻辑,比如路径校验;  3. 代码专注在用户的需求,内容完整、代码简洁;  4. 需要保存 DataFrame 数据的,请使用 excel 格式,确保文件编码正确;其他的保存成对应格式的文本文件;    5. 需要打印出分析结果  6. 只生成一份文件,文件名称不要和输入的文件名一致,文件名为中文    输出文件写入到 {{ output_dir }} 这个目录下,目录文件已经创建,不需要再判断路径是否存在以及创建输出路径    你的任务如下:  {{ task }}

3.6 深度搜索工具(DeepSearchTool)

3.6.1 工具架构设计

深度搜索工具实现了智能体的信息检索能力,通过多源搜索、内容去重、智能摘要等技术,为智能体提供准确、全面的信息支持。

3.6.2 Java端实现

package com.jd.genie.agent.tool.common;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.dto.DeepSearchRequest;import com.jd.genie.agent.dto.DeepSearchrResponse;import com.jd.genie.agent.dto.FileRequest;import com.jd.genie.agent.tool.BaseTool;import com.jd.genie.agent.util.SpringContextHolder;import com.jd.genie.agent.util.StringUtil;import com.jd.genie.config.GenieConfig;import lombok.Data;import lombok.extern.slf4j.Slf4j;import okhttp3.*;import org.springframework.context.ApplicationContext;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.Collections;import java.util.HashMap;import java.util.Map;import java.util.concurrent.CompletableFuture;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;@Slf4j@Datapublic class DeepSearchTool implements BaseTool {    private AgentContext agentContext;    @Override    public String getName() {        return "deep_search";    }    @Override    public String getDescription() {        String desc = "这是一个搜索工具,可以通过搜索内外网知识";        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        return genieConfig.getDeepSearchToolDesc().isEmpty() ? desc : genieConfig.getDeepSearchToolDesc();    }    @Override    public Map<String, Object> toParams() {        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        if (!genieConfig.getDeepSearchToolPamras().isEmpty()) {            return genieConfig.getDeepSearchToolPamras();        }        Map<String, Object> taskParam = new HashMap<>();        taskParam.put("type", "string");        taskParam.put("description", "需要搜索的query");        Map<String, Object> parameters = new HashMap<>();        parameters.put("type", "object");        Map<String, Object> properties = new HashMap<>();        properties.put("query", taskParam);        parameters.put("properties", properties);        parameters.put("required", Collections.singletonList("query"));        return parameters;    }    @Override    public Object execute(Object input) {        long startTime = System.currentTimeMillis();        try {            GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);            Map<String, Object> params = (Map<String, Object>) input;            String query = (String) params.get("query");            Map<String, Object> srcConfig = new HashMap<>();            Map<String, Object> bingConfig = new HashMap<>();            bingConfig.put("count", Integer.parseInt(genieConfig.getDeepSearchPageCount()));            srcConfig.put("bing", bingConfig);            DeepSearchRequest request = DeepSearchRequest.builder()                    .request_id(agentContext.getRequestId() + ":" + StringUtil.generateRandomString(5))                    .query(query)                    .agent_id("1")                    .scene_type("auto_agent")                    .src_configs(srcConfig)                    .stream(true)                    .content_stream(agentContext.getIsStream())                    .build();            // 调用流式 API            Future future = callDeepSearchStream(request);            Object object = future.get();            return object;        } catch (Exception e) {            log.error("{} deep_search agent error", agentContext.getRequestId(), e);        }        return null;    }    /**     * 调用 DeepSearch     */    public CompletableFuture<String> callDeepSearchStream(DeepSearchRequest searchRequest) {        CompletableFuture<String> future = new CompletableFuture<>();        try {            OkHttpClient client = new OkHttpClient.Builder()                    .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为 60 秒                    .readTimeout(300, TimeUnit.SECONDS)    // 设置读取超时时间为 300 秒                    .writeTimeout(300, TimeUnit.SECONDS)   // 设置写入超时时间为 300 秒                    .callTimeout(300, TimeUnit.SECONDS)    // 设置调用超时时间为 300 秒                    .build();            ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();            GenieConfig genieConfig = applicationContext.getBean(GenieConfig.class);            String url = genieConfig.getDeepSearchUrl() + "/v1/tool/deepsearch";            RequestBody body = RequestBody.create(                    MediaType.parse("application/json"),                    JSONObject.toJSONString(searchRequest)            );            log.info("{} deep_search request {}", agentContext.getRequestId(), JSONObject.toJSONString(searchRequest));            Request.Builder requestBuilder = new Request.Builder()                    .url(url)                    .post(body);            Request request = requestBuilder.build();            String[] interval = genieConfig.getMessageInterval().getOrDefault("search", "5,20").split(",");            int firstInterval = Integer.parseInt(interval[0]);            int sendInterval = Integer.parseInt(interval[1]);            client.newCall(request).enqueue(new Callback() {                @Override                public void onFailure(Call call, IOException e) {                    log.error("{} deep_search on failure", agentContext.getRequestId(), e);                    future.completeExceptionally(e);                }                @Override                public void onResponse(Call call, Response response) {                    log.info("{} deep_search response {} {} {}", agentContext.getRequestId(), response, response.code(), response.body());                    try (ResponseBody responseBody = response.body()) {                        if (!response.isSuccessful() || responseBody == null) {                            log.error("{} deep_search request error", agentContext.getRequestId());                            future.completeExceptionally(new IOException("Unexpected response code: " + response));                            return;                        }                        int index = 1;                        StringBuilder stringBuilderIncr = new StringBuilder();                        StringBuilder stringBuilderAll = new StringBuilder();                        String line;                        BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody.byteStream()));                        String digitalEmployee = agentContext.getToolCollection().getDigitalEmployee(getName());                        String result = "搜索结果为空"; // 默认输出                        String messageId = "";                        while ((line = reader.readLine()) != null) {                            if (line.startsWith("data: ")) {                                String data = line.substring(6);                                if (data.equals("[DONE]")) {                                    break;                                }                                if (data.startsWith("heartbeat")) {                                    continue;                                }                                if (index == 1 || index % 100 == 0) {                                    log.info("{} deep_search recv data: {}", agentContext.getRequestId(), data);                                }                                DeepSearchrResponse searchResponse = JSONObject.parseObject(data, DeepSearchrResponse.class);                                FileTool fileTool = new FileTool();                                fileTool.setAgentContext(agentContext);                                // 上传搜索内容到文件中                                if (searchResponse.getIsFinal()) {                                    if (agentContext.getIsStream()) {                                        searchResponse.setAnswer(stringBuilderAll.toString());                                    }                                    if (searchResponse.getAnswer().isEmpty()) {                                        log.error("{} deep search answer empty", agentContext.getRequestId());                                        break;                                    }                                    String fileName = StringUtil.removeSpecialChars(searchResponse.getQuery() + "的搜索结果.md");                                    String fileDesc = searchResponse.getAnswer()                                            .substring(0, Math.min(searchResponse.getAnswer().length(), genieConfig.getDeepSearchToolFileDescTruncateLen())) + "...";                                    FileRequest fileRequest = FileRequest.builder()                                            .requestId(agentContext.getRequestId())                                            .fileName(fileName)                                            .description(fileDesc)                                            .content(searchResponse.getAnswer())                                            .build();                                    fileTool.uploadFile(fileRequest, false, false);                                    result = searchResponse.getAnswer().                                            substring(0, Math.min(searchResponse.getAnswer().length(), genieConfig.getDeepSearchToolMessageTruncateLen()));                                    agentContext.getPrinter().send(messageId, "deep_search", searchResponse, digitalEmployee, true);                                } else {                                    Map<String, Object> contentMap = new HashMap<>();                                    for (int idx = 0; idx < searchResponse.getSearchResult().getQuery().size(); idx++) {                                        contentMap.put(searchResponse.getSearchResult().getQuery().get(idx), searchResponse.getSearchResult().getDocs().get(idx));                                    }                                    if ("extend".equals(searchResponse.getMessageType())) {                                        messageId = StringUtil.getUUID();                                        searchResponse.setSearchFinish(false);                                        agentContext.getPrinter().send(messageId, "deep_search", searchResponse, digitalEmployee, true);                                    } else if ("search".equals(searchResponse.getMessageType())) {                                        searchResponse.setSearchFinish(true);                                        agentContext.getPrinter().send(messageId, "deep_search", searchResponse, digitalEmployee, true);                                        FileRequest fileRequest = FileRequest.builder()                                                .requestId(agentContext.getRequestId())                                                .fileName(searchResponse.getQuery() + "_search_result.txt")                                                .description(searchResponse.getQuery() + "...")                                                .content(JSON.toJSONString(contentMap))                                                .build();                                        fileTool.uploadFile(fileRequest, false, true);                                    } else if ("report".equals(searchResponse.getMessageType())) {                                        if (index == 1) {                                            messageId = StringUtil.getUUID();                                        }                                        stringBuilderIncr.append(searchResponse.getAnswer());                                        stringBuilderAll.append(searchResponse.getAnswer());                                        if (index == firstInterval || index % sendInterval == 0) {                                            searchResponse.setAnswer(stringBuilderIncr.toString());                                            agentContext.getPrinter().send(messageId, "deep_search", searchResponse, digitalEmployee, false);                                            stringBuilderIncr.setLength(0);                                        }                                        index++;                                    }                                }                            }                        }                        future.complete(result);                    } catch (Exception e) {                        log.error("{} deep_search request error", agentContext.getRequestId(), e);                        future.completeExceptionally(e);                    }                }            });        } catch (Exception e) {            log.error("{} deep_search request error", agentContext.getRequestId(), e);            future.completeExceptionally(e);        }        return future;    }}

3.6.3 搜索引擎抽象

Python端实现了搜索引擎的抽象基类:

class SearchBase(ABC):    """搜索基类"""    def __init__(self):        self._count = int(os.getenv("SEARCH_COUNT", 10))        self._timeout = int(os.getenv("SEARCH_TIMEOUT", 10))        self._use_jd_gateway = os.getenv("USE_JD_SEARCH_GATEWAY", "true") == "true"    @abstractmethod    async def search(self, query: str, request_id: str = None, *args, **kwargs) -> List[Doc]:        """抽象搜索方法"""        raise NotImplementedError    @staticmethod    @timer()    async def parser(docs: List[Doc], timeout: int=10, **kwargs) -> List[Doc]:        """解析搜索结果"""        # 实现解析逻辑        pass

3.7 报告生成工具(ReportTool)

3.7.1 多格式报告支持

报告生成工具支持HTML、Markdown、PPT等多种格式的报告输出:

package com.jd.genie.agent.tool.common;import com.alibaba.fastjson.JSONObject;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.dto.CodeInterpreterRequest;import com.jd.genie.agent.dto.CodeInterpreterResponse;import com.jd.genie.agent.dto.File;import com.jd.genie.agent.tool.BaseTool;import com.jd.genie.agent.util.SpringContextHolder;import com.jd.genie.agent.util.StringUtil;import com.jd.genie.config.GenieConfig;import lombok.Data;import lombok.extern.slf4j.Slf4j;import okhttp3.*;import org.springframework.context.ApplicationContext;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.*;import java.util.concurrent.CompletableFuture;import java.util.concurrent.Future;import java.util.concurrent.TimeUnit;import java.util.stream.Collectors;@Slf4j@Datapublic class ReportTool implements BaseTool {    private AgentContext agentContext;    @Override    public String getName() {        return "report_tool";    }    @Override    public String getDescription() {        String desc = "这是一个报告工具,可以通过编写HTML、MarkDown报告";        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        return genieConfig.getReportToolDesc().isEmpty() ? desc : genieConfig.getReportToolDesc();    }    @Override    public Map<String, Object> toParams() {        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        if (!genieConfig.getReportToolPamras().isEmpty()) {            return genieConfig.getReportToolPamras();        }        Map<String, Object> taskParam = new HashMap<>();        taskParam.put("type", "string");        taskParam.put("description", "需要完成的任务以及完成任务需要的数据,需要尽可能详细");        Map<String, Object> parameters = new HashMap<>();        parameters.put("type", "object");        Map<String, Object> properties = new HashMap<>();        properties.put("task", taskParam);        parameters.put("properties", properties);        parameters.put("required", Collections.singletonList("task"));        return parameters;    }    @Override    public Object execute(Object input) {        long startTime = System.currentTimeMillis();        try {            Map<String, Object> params = (Map<String, Object>) input;            String task = (String) params.get("task");            String fileDescription = (String) params.get("fileDescription");            String fileName = (String) params.get("fileName");            String fileType = (String) params.get("fileType");            if (fileName.isEmpty()) {                String errMessage = "文件名参数为空,无法生成报告。";                log.error("{} {}", agentContext.getRequestId(), errMessage);                return null;            }            List<String> fileNames = agentContext.getProductFiles().stream().map(File::getFileName).collect(Collectors.toList());            Map<String, Object> streamMode = new HashMap<>();            streamMode.put("mode", "token");            streamMode.put("token", 10);            CodeInterpreterRequest request = CodeInterpreterRequest.builder()                    .requestId(agentContext.getSessionId()) // 适配多轮对话                    .query(agentContext.getQuery())                    .task(task)                    .fileNames(fileNames)                    .fileName(fileName)                    .fileDescription(fileDescription)                    .stream(true)                    .contentStream(agentContext.getIsStream())                    .streamMode(streamMode)                    .fileType(fileType)                    .build();            // 调用流式 API            Future future = callCodeAgentStream(request);            Object object = future.get();            return object;        } catch (Exception e) {            log.error("{} report_tool error", agentContext.getRequestId(), e);        }        return null;    }    /**     * 调用 CodeAgent     */    public CompletableFuture<String> callCodeAgentStream(CodeInterpreterRequest codeRequest) {        CompletableFuture<String> future = new CompletableFuture<>();        try {            OkHttpClient client = new OkHttpClient.Builder()                    .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为 1 分钟                    .readTimeout(600, TimeUnit.SECONDS)    // 设置读取超时时间为 10 分钟                    .writeTimeout(600, TimeUnit.SECONDS)   // 设置写入超时时间为 10 分钟                    .callTimeout(600, TimeUnit.SECONDS)    // 设置调用超时时间为 10 分钟                    .build();            ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();            GenieConfig genieConfig = applicationContext.getBean(GenieConfig.class);            String url = genieConfig.getCodeInterpreterUrl() + "/v1/tool/report";            RequestBody body = RequestBody.create(                    MediaType.parse("application/json"),                    JSONObject.toJSONString(codeRequest)            );            log.info("{} report_tool request {}", agentContext.getRequestId(), JSONObject.toJSONString(codeRequest));            Request.Builder requestBuilder = new Request.Builder()                    .url(url)                    .post(body);            Request request = requestBuilder.build();            String[] interval = genieConfig.getMessageInterval().getOrDefault("report", "1,4").split(",");            int firstInterval = Integer.parseInt(interval[0]);            int sendInterval = Integer.parseInt(interval[1]);            client.newCall(request).enqueue(new Callback() {                @Override                public void onFailure(Call call, IOException e) {                    log.error("{} report_tool on failure", agentContext.getRequestId(), e);                    future.completeExceptionally(e);                }                @Override                public void onResponse(Call call, Response response) {                    log.info("{} report_tool response {} {} {}", agentContext.getRequestId(), response, response.code(), response.body());                    CodeInterpreterResponse codeResponse = CodeInterpreterResponse.builder()                            .codeOutput("report_tool 执行失败") // 默认输出                            .build();                    try {                        ResponseBody responseBody = response.body();                        if (!response.isSuccessful() || responseBody == null) {                            log.error("{} report_tool request error.", agentContext.getRequestId());                            future.completeExceptionally(new IOException("Unexpected response code: " + response));                            return;                        }                        int index = 1;                        StringBuilder stringBuilderIncr = new StringBuilder();                        String line;                        String messageId = StringUtil.getUUID();                        // 获取数字人名称                        String digitalEmployee = agentContext.getToolCollection().getDigitalEmployee(getName());                        BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody.byteStream()));                        while ((line = reader.readLine()) != null) {                            if (line.startsWith("data: ")) {                                String data = line.substring(6);                                if (data.equals("[DONE]")) {                                    break;                                }                                if (index == 1 || index % 100 == 0) {                                    log.info("{} report_tool recv data: {}", agentContext.getRequestId(), data);                                }                                if (data.startsWith("heartbeat")) {                                    continue;                                }                                codeResponse = JSONObject.parseObject(data, CodeInterpreterResponse.class);                                if (codeResponse.getIsFinal()) {                                    // report_tool 只会输出一个文件,使用模型输出的文件名和描述                                    if (Objects.nonNull(codeResponse.getFileInfo())) {                                        for (CodeInterpreterResponse.FileInfo fileInfo : codeResponse.getFileInfo()) {                                            File file = File.builder()                                                    .fileName(codeRequest.getFileName())                                                    .fileSize(fileInfo.getFileSize())                                                    .ossUrl(fileInfo.getOssUrl())                                                    .domainUrl(fileInfo.getDomainUrl())                                                    .description(codeRequest.getFileDescription())                                                    .isInternalFile(false)                                                    .build();                                            agentContext.getProductFiles().add(file);                                            agentContext.getTaskProductFiles().add(file);                                        }                                    }                                    agentContext.getPrinter().send(messageId, codeRequest.getFileType(), codeResponse, digitalEmployee, true);                                } else {                                    stringBuilderIncr.append(codeResponse.getData());                                    if (index == firstInterval || index % sendInterval == 0) {                                        codeResponse.setData(stringBuilderIncr.toString());                                        agentContext.getPrinter().send(messageId, codeRequest.getFileType(), codeResponse, digitalEmployee, false);                                        stringBuilderIncr.setLength(0);                                    }                                }                                index++;                            }                        }                    } catch (Exception e) {                        log.error("{} report_tool request error", agentContext.getRequestId(), e);                        future.completeExceptionally(e);                        return;                    }                    // 统一使用data字段,兼容历史codeOutput逻辑                    String result = Objects.nonNull(codeResponse.getData()) && !codeResponse.getData().isEmpty() ? codeResponse.getData() : codeResponse.getCodeOutput();                    future.complete(result);                }            });        } catch (Exception e) {            log.error("{} report_tool request error", agentContext.getRequestId(), e);            future.completeExceptionally(e);        }        return future;    }}

3.8 文件处理工具(FileTool)

3.8.1 文件生命周期管理

文件处理工具实现了完整的文件生命周期管理:

package com.jd.genie.agent.tool.common;import com.alibaba.fastjson.JSON;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.dto.CodeInterpreterResponse;import com.jd.genie.agent.dto.File;import com.jd.genie.agent.dto.FileRequest;import com.jd.genie.agent.dto.FileResponse;import com.jd.genie.agent.tool.BaseTool;import com.jd.genie.agent.util.SpringContextHolder;import com.jd.genie.agent.util.StringUtil;import com.jd.genie.config.GenieConfig;import lombok.Data;import lombok.extern.slf4j.Slf4j;import okhttp3.*;import org.springframework.context.ApplicationContext;import java.io.IOException;import java.util.*;import java.util.concurrent.TimeUnit;@Slf4j@Datapublic class FileTool implements BaseTool {    private AgentContext agentContext;    @Override    public String getName() {        return "file_tool";    }    @Override    public String getDescription() {        String desc = "这是一个文件工具,可以上传或下载文件";        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        return genieConfig.getFileToolDesc().isEmpty() ? desc : genieConfig.getFileToolDesc();    }    @Override    public Map<String, Object> toParams() {        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        if (!genieConfig.getFileToolDesc().isEmpty()) {            return genieConfig.getFileToolPamras();        }        Map<String, Object> command = new HashMap<>();        command.put("type", "string");        command.put("description", "文件操作类型:upload、get");        Map<String, Object> fileName = new HashMap<>();        fileName.put("type", "string");        fileName.put("description", "文件名");        Map<String, Object> fileDesc = new HashMap<>();        fileDesc.put("type", "string");        fileDesc.put("description", "文件描述,20字左右,upload时必填");        Map<String, Object> fileContent = new HashMap<>();        fileContent.put("type", "string");        fileContent.put("description", "文件内容,upload时必填");        Map<String, Object> parameters = new HashMap<>();        parameters.put("type", "object");        Map<String, Object> properties = new HashMap<>();        properties.put("command", command);        properties.put("filename", fileName);        properties.put("description", fileDesc);        properties.put("content", fileContent);        parameters.put("properties", properties);        parameters.put("required", Arrays.asList("command", "filename"));        return parameters;    }    @Override    public Object execute(Object input) {        try {            Map<String, Object> params = (Map<String, Object>) input;            String command = (String) params.getOrDefault("command", "");            FileRequest fileRequest = JSON.parseObject(JSON.toJSONString(input), FileRequest.class);            fileRequest.setRequestId(agentContext.getRequestId());            if ("upload".equals(command)) {                return uploadFile(fileRequest, true, false);            } else if ("get".equals(command)) {                return getFile(fileRequest, true);            }        } catch (Exception e) {            log.error("{} file tool error", agentContext.getRequestId(), e);        }        return null;    }    // 上传文件的 API 请求方法    public String uploadFile(FileRequest fileRequest, Boolean isNoticeFe, Boolean isInternalFile) {        long startTime = System.currentTimeMillis();        OkHttpClient client = new OkHttpClient.Builder()                .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为 60 秒                .readTimeout(300, TimeUnit.SECONDS)    // 设置读取超时时间为 300 秒                .writeTimeout(300, TimeUnit.SECONDS)   // 设置写入超时时间为 300 秒                .callTimeout(300, TimeUnit.SECONDS)    // 设置调用超时时间为 300 秒                .build();        ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();        GenieConfig genieConfig = applicationContext.getBean(GenieConfig.class);        MediaType mediaType = MediaType.get("application/json; charset=utf-8");        String url = genieConfig.getCodeInterpreterUrl() + "/v1/file_tool/upload_file";        // 构建请求体 多轮对话替换requestId为sessionId        fileRequest.setRequestId(agentContext.getSessionId());        // 清理文件名中的特殊字符        fileRequest.setFileName(StringUtil.removeSpecialChars(fileRequest.getFileName()));        if (fileRequest.getFileName().isEmpty()) {            String errorMessage = "上传文件失败 文件名为空";            log.error("{} {}", agentContext.getRequestId(), errorMessage);            return null;        }        RequestBody body = RequestBody.create(JSON.toJSONString(fileRequest), mediaType);        Request request = new Request.Builder()                .url(url)                .post(body)                .addHeader("Content-Type", "application/json")                .build();        try {            log.info("{} file tool upload request {}", agentContext.getRequestId(), JSON.toJSONString(fileRequest));            Response response = client.newCall(request).execute();            if (!response.isSuccessful() || response.body() == null) {                log.error("{} upload file faied", agentContext.getRequestId());                return null;            }            String result = response.body().string();            FileResponse fileResponse = JSON.parseObject(result, FileResponse.class);            log.info("{} file tool upload response {}", agentContext.getRequestId(), result);            // 构建前端格式            Map<String, Object> resultMap = new HashMap<>();            resultMap.put("command", "写入文件");            List<CodeInterpreterResponse.FileInfo> fileInfo = new ArrayList<>();            fileInfo.add(CodeInterpreterResponse.FileInfo.builder()                    .fileName(fileRequest.getFileName())                    .ossUrl(fileResponse.getOssUrl())                    .domainUrl(fileResponse.getDomainUrl())                    .fileSize(fileResponse.getFileSize())                    .build());            resultMap.put("fileInfo", fileInfo);            // 获取数字人            String digitalEmployee = agentContext.getToolCollection().getDigitalEmployee(getName());            log.info("requestId:{} task:{} toolName:{} digitalEmployee:{}", agentContext.getRequestId(),                    agentContext.getToolCollection().getCurrentTask(), getName(), digitalEmployee);            // 添加文件到上下文            File file = File.builder()                    .ossUrl(fileResponse.getOssUrl())                    .domainUrl(fileResponse.getDomainUrl())                    .fileName(fileRequest.getFileName())                    .fileSize(fileResponse.getFileSize())                    .description(fileRequest.getDescription())                    .isInternalFile(isInternalFile)                    .build();            agentContext.getProductFiles().add(file);            if (isNoticeFe) {                // 内部文件不通知前端                agentContext.getPrinter().send("file", resultMap, digitalEmployee);            }            if (!isInternalFile) {                // 非内部文件,参与交付物                agentContext.getTaskProductFiles().add(file);            }            // 返回工具执行结果            return fileRequest.getFileName() + " 写入到文件链接: " + fileResponse.getOssUrl();        } catch (Exception e) {            log.error("{} upload file error", agentContext.getRequestId(), e);        }        return null;    }    // 获取文件的 API 请求方法    public String getFile(FileRequest fileRequest, Boolean noticeFe) {        long startTime = System.currentTimeMillis();        OkHttpClient client = new OkHttpClient.Builder()                .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为 60 秒                .readTimeout(300, TimeUnit.SECONDS)    // 设置读取超时时间为 60 秒                .writeTimeout(300, TimeUnit.SECONDS)   // 设置写入超时时间为 60 秒                .callTimeout(300, TimeUnit.SECONDS)    // 设置调用超时时间为 60 秒                .build();        ApplicationContext applicationContext = SpringContextHolder.getApplicationContext();        GenieConfig genieConfig = applicationContext.getBean(GenieConfig.class);        MediaType mediaType = MediaType.get("application/json; charset=utf-8");        String url = genieConfig.getCodeInterpreterUrl() + "/v1/file_tool/get_file";        // 构建请求体        FileRequest getFileRequest = FileRequest.builder()                .requestId(agentContext.getRequestId())                .fileName(fileRequest.getFileName())                .build();        // 适配多轮对话        getFileRequest.setRequestId(agentContext.getSessionId());        RequestBody body = RequestBody.create(JSON.toJSONString(getFileRequest), mediaType);        Request request = new Request.Builder()                .url(url)                .post(body)                .addHeader("Content-Type", "application/json")                .build();        try {            log.info("{} file tool get request {}", agentContext.getRequestId(), JSON.toJSONString(getFileRequest));            Response response = client.newCall(request).execute();            if (!response.isSuccessful() || response.body() == null) {                String errMessage = "获取文件失败 " + fileRequest.getFileName();                return errMessage;            }            String result = response.body().string();            FileResponse fileResponse = JSON.parseObject(result, FileResponse.class);            log.info("{} file tool get response {}", agentContext.getRequestId(), result);            // 构建前端格式            Map<String, Object> resultMap = new HashMap<>();            resultMap.put("command", "读取文件");            List<CodeInterpreterResponse.FileInfo> fileInfo = new ArrayList<>();            fileInfo.add(CodeInterpreterResponse.FileInfo.builder()                    .fileName(fileRequest.getFileName())                    .ossUrl(fileResponse.getOssUrl())                    .domainUrl(fileResponse.getDomainUrl())                    .fileSize(fileResponse.getFileSize())                    .build());            resultMap.put("fileInfo", fileInfo);            // 获取数字人            String digitalEmployee = agentContext.getToolCollection().getDigitalEmployee(getName());            log.info("requestId:{} task:{} toolName:{} digitalEmployee:{}", agentContext.getRequestId(),                    agentContext.getToolCollection().getCurrentTask(), getName(), digitalEmployee);            // 通知前端            if (noticeFe) {                agentContext.getPrinter().send("file", resultMap, digitalEmployee);            }            // 返回工具执行结果            String fileContent = getUrlContent(fileResponse.getOssUrl());            if (Objects.nonNull(fileContent)) {                if (fileContent.length() > genieConfig.getFileToolContentTruncateLen()) {                    fileContent = fileContent.substring(0, genieConfig.getFileToolContentTruncateLen());                }                return "文件内容 " + fileContent;            }        } catch (Exception e) {            log.error("{} get file error", agentContext.getRequestId(), e);        }        return null;    }    private String getUrlContent(String url) {        OkHttpClient client = new OkHttpClient.Builder()                .connectTimeout(60, TimeUnit.SECONDS) // 设置连接超时时间为 60 秒                .readTimeout(60, TimeUnit.SECONDS)    // 设置读取超时时间为 60 秒                .writeTimeout(60, TimeUnit.SECONDS)   // 设置写入超时时间为 60 秒                .callTimeout(60, TimeUnit.SECONDS)    // 设置调用超时时间为 60 秒                .build();        Request request = new Request.Builder()                .url(url)                .build();        try (Response response = client.newCall(request).execute()) {            if (response.isSuccessful() && response.body() != null) {                return response.body().string();            } else {                String errMsg = String.format("获取文件失败, 状态码:%d", response.code());                log.error("{} 获取文件失败 {}", agentContext.getRequestId(), response.code());                return null;            }        } catch (IOException e) {            log.error("{} 获取文件异常", agentContext.getRequestId(), e);            return null;        }    }}

3.9 规划工具(PlanningTool)

3.9.1 计划生命周期管理

规划工具实现了完整的计划生命周期管理,支持创建、更新、执行状态跟踪:

package com.jd.genie.agent.tool.common;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.dto.Plan;import com.jd.genie.agent.tool.BaseTool;import com.jd.genie.agent.util.SpringContextHolder;import com.jd.genie.config.GenieConfig;import lombok.Data;import java.util.*;import java.util.function.Function;/** * 计划工具类 */@Datapublic class PlanningTool implements BaseTool {    private AgentContext agentContext;    private final Map<String, Function<Map<String, Object>, String>> commandHandlers = new HashMap<>();    private Plan plan;    public PlanningTool() {        commandHandlers.put("create", this::createPlan);        commandHandlers.put("update", this::updatePlan);        commandHandlers.put("mark_step", this::markStep);        commandHandlers.put("finish", this::finishPlan);    }    @Override    public String getName() {        return "planning";    }    @Override    public String getDescription() {        String desc = "这是一个计划工具,可让代理创建和管理用于解决复杂任务的计划。\n该工具提供创建计划、更新计划步骤和跟踪进度的功能。\n使用中文回答";        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        return genieConfig.getPlanToolDesc().isEmpty() ? desc : genieConfig.getPlanToolDesc();    }    @Override    public Map<String, Object> toParams() {        GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);        if (!genieConfig.getPlanToolParams().isEmpty()) {            return genieConfig.getPlanToolParams();        }        return getParameters();    }    private Map<String, Object> getParameters() {        Map<String, Object> parameters = new HashMap<>();        parameters.put("type", "object");        parameters.put("properties", getProperties());        parameters.put("required", List.of("command"));        return parameters;    }    private Map<String, Object> getProperties() {        Map<String, Object> properties = new HashMap<>();        properties.put("command", getCommandProperty());        properties.put("title", getTitleProperty());        properties.put("steps", getStepsProperty());        properties.put("step_index", getStepIndexProperty());        properties.put("step_status", getStepStatusProperty());        properties.put("step_notes", getStepNotesProperty());        return properties;    }    private Map<String, Object> getCommandProperty() {        Map<String, Object> command = new HashMap<>();        command.put("type", "string");        command.put("enum", Arrays.asList("create", "update", "mark_step", "finish"));        command.put("description", "The command to execute. Available commands: create, update, mark_step, finish");        return command;    }    private Map<String, Object> getTitleProperty() {        Map<String, Object> title = new HashMap<>();        title.put("type", "string");        title.put("description", "Title for the plan. Required for create command, optional for update command.");        return title;    }    private Map<String, Object> getStepsProperty() {        Map<String, Object> items = new HashMap<>();        items.put("type", "string");        Map<String, Object> command = new HashMap<>();        command.put("type", "array");        command.put("items", items);        command.put("description", "List of plan steps. Required for create command, optional for update command.");        return command;    }    private Map<String, Object> getStepIndexProperty() {        Map<String, Object> stepIndex = new HashMap<>();        stepIndex.put("type", "integer");        stepIndex.put("description", "Index of the step to update (0-based). Required for mark_step command.");        return stepIndex;    }    private Map<String, Object> getStepStatusProperty() {        Map<String, Object> stepStatus = new HashMap<>();        stepStatus.put("type", "string");        stepStatus.put("enum", Arrays.asList("not_started", "in_progress", "completed", "blocked"));        stepStatus.put("description", "Status to set for a step. Used with mark_step command.");        return stepStatus;    }    private Map<String, Object> getStepNotesProperty() {        Map<String, Object> stepNotes = new HashMap<>();        stepNotes.put("type", "string");        stepNotes.put("description", "Additional notes for a step. Optional for mark_step command.");        return stepNotes;    }    @Override    public Object execute(Object input) {        if (!(input instanceof Map)) {            throw new IllegalArgumentException("Input must be a Map");        }        Map<String, Object> params = (Map<String, Object>) input;        String command = (String) params.get("command");        if (command == null || command.isEmpty()) {            throw new IllegalArgumentException("Command is required");        }        Function<Map<String, Object>, String> handler = commandHandlers.get(command);        if (handler != null) {            return handler.apply(params);        } else {            throw new IllegalArgumentException("Unknown command: " + command);        }    }    private String createPlan(Map<String, Object> params) {        String title = (String) params.get("title");        List<String> steps = (List<String>) params.get("steps");        if (title == null || steps == null) {            throw new IllegalArgumentException("title, and steps are required for create command");        }        if (plan != null) {            throw new IllegalStateException("A plan already exists. Delete the current plan first.");        }        plan = Plan.create(title, steps);        return "我已创建plan";    }    private String updatePlan(Map<String, Object> params) {        String title = (String) params.get("title");        List<String> steps = (List<String>) params.get("steps");        if (plan == null) {            throw new IllegalStateException("No plan exists. Create a plan first.");        }        plan.update(title, steps);        return "我已更新plan";    }    private String markStep(Map<String, Object> params) {        Integer stepIndex = (Integer) params.get("step_index");        String stepStatus = (String) params.get("step_status");        String stepNotes = (String) params.get("step_notes");        if (plan == null) {            throw new IllegalStateException("No plan exists. Create a plan first.");        }        if (stepIndex == null) {            throw new IllegalArgumentException("step_index is required for mark_step command");        }        plan.updateStepStatus(stepIndex, stepStatus, stepNotes);        return String.format("我已标记plan %d 为 %s", stepIndex, stepStatus);    }    private String finishPlan(Map<String, Object> params) {        if (Objects.isNull(plan)) {            plan = new Plan();        } else {            for (int stepIndex = 0; stepIndex < plan.getSteps().size(); stepIndex++) {                plan.updateStepStatus(stepIndex, "completed", "");            }        }        return "我已更新plan为完成状态";    }    public void stepPlan() {        plan.stepPlan();    }    public String getFormatPlan() {        if (plan == null) {            return "目前还没有Plan";        }        return plan.format();    }}

第三部分:MCP工具生态与扩展机制 🌐

3.10 MCP协议集成

3.10.1 MCP工具抽象

JoyAgent-JDGenie通过MCP(Model Context Protocol)协议实现了第三方工具的无缝集成:

package com.jd.genie.agent.tool.mcp;import com.alibaba.fastjson.JSON;import com.jd.genie.agent.agent.AgentContext;import com.jd.genie.agent.tool.BaseTool;import com.jd.genie.agent.util.OkHttpUtil;import com.jd.genie.agent.util.SpringContextHolder;import com.jd.genie.config.GenieConfig;import lombok.AllArgsConstructor;import lombok.Builder;import lombok.Data;import lombok.NoArgsConstructor;import lombok.extern.slf4j.Slf4j;import java.util.Map;@Slf4j@Datapublic class McpTool implements BaseTool {    private AgentContext agentContext;    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public static class McpToolRequest {        private String server_url;        private String name;        private Map<String, Object> arguments;    }    @Data    @Builder    @NoArgsConstructor    @AllArgsConstructor    public static class McpToolResponse {        private String code;        private String message;        private String data;    }    @Override    public String getName() {        return "mcp_tool";    }    @Override    public String getDescription() {        return "";    }    @Override    public Map<String, Object> toParams() {        return null;    }    @Override    public Object execute(Object input) {        return null;    }    public String listTool(String mcpServerUrl) {        try {            GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);            String mcpClientUrl = genieConfig.getMcpClientUrl() + "/v1/tool/list";            McpToolRequest mcpToolRequest = McpToolRequest.builder()                    .server_url(mcpServerUrl)                    .build();            String response = OkHttpUtil.postJson(mcpClientUrl, JSON.toJSONString(mcpToolRequest), null, 30L);            log.info("list tool request: {} response: {}", JSON.toJSONString(mcpToolRequest), response);            return response;        } catch (Exception e) {            log.error("{} list tool error", agentContext.getRequestId(), e);        }        return "";    }    public String callTool(String mcpServerUrl, String toolName, Object input) {        try {            GenieConfig genieConfig = SpringContextHolder.getApplicationContext().getBean(GenieConfig.class);            String mcpClientUrl = genieConfig.getMcpClientUrl() + "/v1/tool/call";            Map<String, Object> params = (Map<String, Object>) input;            McpToolRequest mcpToolRequest = McpToolRequest.builder()                    .name(toolName)                    .server_url(mcpServerUrl)                    .arguments(params)                    .build();            String response = OkHttpUtil.postJson(mcpClientUrl, JSON.toJSONString(mcpToolRequest), null, 30L);            log.info("call tool request: {} response: {}", JSON.toJSONString(mcpToolRequest), response);            return response;        } catch (Exception e) {            log.error("{} call tool error ", agentContext.getRequestId(), e);        }        return "";    }}

3.10.2 MCP工具信息管理

通过McpToolInfo数据结构管理MCP工具的元数据:

@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class McpToolInfo {    private String name;    private String desc;     private String parameters;    private String mcpServerUrl;}

3.11 工具进化机制

3.11.1 原子工具拆解

JoyAgent-JDGenie采用原子工具拆解策略,将复杂工具分解为更小的、可组合的原子单元:

    功能原子化:每个工具专注于单一职责接口标准化:统一的BaseTool接口规范组合灵活性:支持工具间的自由组合

3.11.2 工具自动组合

系统支持根据任务需求自动组合多个原子工具:

/** * 执行工具 */public Object execute(String name, Object toolInput) {    if (toolMap.containsKey(name)) {        BaseTool tool = getTool(name);        return tool.execute(toolInput);    } else if (mcpToolMap.containsKey(name)) {        McpToolInfo toolInfo = mcpToolMap.get(name);        McpTool mcpTool = new McpTool();        mcpTool.setAgentContext(agentContext);        return mcpTool.callTool(toolInfo.getMcpServerUrl(), name, toolInput);    } else {        log.error("Error: Unknown tool {}", name);    }    return null;}

3.11.3 工具效果评估

系统通过多维度指标评估工具执行效果:

    执行成功率:工具调用的成功与失败统计响应时间:工具执行的性能指标结果质量:工具输出结果的准确性评估用户满意度:基于用户反馈的工具评价

3.12 Python工具服务架构

3.12.1 FastAPI服务设计

Python工具服务采用FastAPI框架,提供高性能的异步Web服务:

from fastapi import APIRouterfrom sse_starlette import ServerSentEvent, EventSourceResponsefrom genie_tool.model.code import ActionOutput, CodeOuputfrom genie_tool.model.protocal import CIRequest, ReportRequest, DeepSearchRequestfrom genie_tool.util.file_util import upload_filefrom genie_tool.tool.report import reportfrom genie_tool.tool.code_interpreter import code_interpreter_agentfrom genie_tool.util.middleware_util import RequestHandlerRoutefrom genie_tool.tool.deepsearch import DeepSearchrouter = APIRouter(route_class=RequestHandlerRoute)

3.12.2 流式响应处理

支持Server-Sent Events(SSE)实现实时流式响应:

async def _stream():    acc_content = ""    acc_token = 0    acc_time = time.time()    async for chunk in code_interpreter_agent(        task=body.task,        file_names=body.file_names,        request_id=body.request_id,        stream=True,    ):        if isinstance(chunk, CodeOuput):            yield ServerSentEvent(                data=json.dumps(                    {                        "requestId": body.request_id,                        "code": chunk.code,                        "fileInfo": chunk.file_list,                        "isFinal": False,                    },                    ensure_ascii=False,                )            )        # ... 处理其他类型的响应

总结:工具系统设计精髓与扩展思路 🎯

3.13 设计精髓总结

通过对JoyAgent-JDGenie工具系统的深度剖析,我们可以总结出以下设计精髓:

3.13.1 架构设计亮点

    统一抽象接口:BaseTool接口提供了强大而简洁的抽象,所有工具都遵循相同的契约分层架构清晰:Java后端负责调度管理,Python服务负责具体执行,职责分离明确插件化扩展:通过MCP协议实现第三方工具的无缝集成,生态开放性强流式处理支持:全面支持流式响应,提升用户体验和系统响应性

3.13.2 工具实现特色

    代码解释器:基于smolagents框架,提供安全可靠的Python代码执行环境深度搜索:多源信息检索+智能去重+结构化摘要,信息获取能力强大报告生成:支持多格式输出,满足不同场景的文档需求文件管理:完整的文件生命周期管理,支持上传、下载、内容解析计划工具:支持任务拆解、进度跟踪、状态管理的完整规划能力

3.13.3 技术创新点

    数字员工机制:为不同工具分配专门的数字员工,提供个性化交互体验配置驱动设计:工具描述、参数都支持配置化定制,灵活性极高异步流式架构:Java异步调用+Python异步处理+SSE流式响应,性能优异提示词工程:精心设计的提示词模板,确保工具执行的准确性和一致性

3.14 扩展发展方向

3.14.1 工具生态扩展

    垂直领域工具:开发面向特定行业的专业工具AI能力增强:集成更多AI模型能力(图像、音频、视频处理等)企业级工具:支持企业内部系统的集成(ERP、CRM、OA等)开发者工具:提供代码生成、测试、部署等开发链路工具

3.14.2 架构优化方向

    性能优化:工具执行缓存、并发处理优化、资源池化管理安全增强:工具权限控制、执行沙箱、敏感信息保护可观测性:完善的监控、日志、链路追踪体系容错能力:工具失败重试、降级策略、熔断机制

3.14.3 智能化进阶

    工具自学习:基于执行结果优化工具参数和策略智能组合:AI驱动的工具自动选择和组合效果预测:基于历史数据预测工具执行效果个性化推荐:根据用户习惯推荐最适合的工具

3.15 最佳实践建议

3.15.1 工具开发原则

    单一职责:每个工具专注解决一类特定问题接口规范:严格遵循BaseTool接口契约错误处理:完善的异常处理和错误恢复机制文档完备:清晰的工具描述和使用文档

3.15.2 集成部署要点

    环境隔离:工具执行环境与主系统隔离资源限制:合理设置CPU、内存、时间等资源限制监控告警:实时监控工具执行状态和性能指标版本管理:工具版本控制和灰度发布机制

JoyAgent-JDGenie的工具系统设计为现代多智能体系统提供了完整的工具生态构建方案。通过统一的接口设计、灵活的扩展机制和强大的执行能力,该工具系统不仅满足了当前的功能需求,更为未来的发展和扩展奠定了坚实基础。开发者可以基于这套架构,快速构建适合自己业务场景的工具生态,实现智能体能力的持续演进和提升。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

JoyAgent-JDGenie 工具系统 多智能体 CodeInterpreterTool 架构设计
相关文章