1 line
285 KiB
JSON
1 line
285 KiB
JSON
{"repo": "bbossgroups/bboss-elasticsearch", "files": {"docs/bboss-ai.md": {"0c55a354137310e65e5d7412bd142c4cd26431a5": {"t": 1785768876.8931015, "n": 111731, "h": "4977324348d69056", "c": "# 多模态AI智能体开发框架使用教程\n\n## 一、概述\n\n**bboss-ai** 是一个轻量级 Java AI Agent 开发客户端,基于 Apache HttpClient5、HttpCore5 以及 Project Reactor 构建,提供了对大语言模型(LLM)和多模态模型的统一对接能力,支持同步调用和流式调用两种模式,并内置智能体工作流编排、会话管理、工具搜索等企业级特性。能够快速集成各大主流 AI 模型平台,实现智能问答、图片识别/生成、语音识别/生成、视频识别/生成等功能。\n\n**ClickHouse 生产级会话存储**:bboss-ai 支持基于 ClickHouse 分布式集群的生产级会话持久化能力。使用时需要指定 ClickHouse 集群名称,并为每个集群节点定义名为 `shard` 和 `replica` 的两个宏变量。ClickHouse 模式下会话续问续答时不会更新最后访问时间(受限于 ClickHouse 不支持高频 UPDATE)。详细使用方式参见 [Clickhouse会话存储管理](#chatper14-122) 章节。\n\n### 核心功能\n\n- **模型能力**:兼容 DeepSeek、Kimi、智谱、阿里百炼通义千问、字节豆包火山引擎、MiniMax、腾讯混元、中国移动九天、chatgpt等主流 MaaS 平台;通过简单的适配和扩展即可支持私有化模型平台\n\n- **流式响应**:基于 Reactor 的响应式编程模型,支持背压控制机制\n\n- **工具能力**:支持工具调用和 MCP 服务发现,提供 MCP SSE 和 Streamable 两种通讯协议,同时提供mcp server协议实现\n\n - 工具调用(Function Calling)\n - MCP(Model Context Protocol)服务发现和调用,支持 SSE 和 Streamable HTTP 两种传输模式\n - 可快速发布供LLM调用的工具服务和MCP服务\n - 多轮工具调用(Loop Tool Call),支持智能体自主决策多步骤任务执行\n - 脚本执行工具(Shell/CLI)和代码执行工具(Python/NodeJS)\n - Skills 技能模块,支持技能注册、加载、调度执行及可观测性监测\n - 工具搜索(Tool Searcher),支持基于关键词和语义的工具过滤\n\n- **多模态支持**:支持文本、图片、音频、视频等多种模态的识别与生成\n\n - 图片识别与生成(Vision/Image Generation)\n - 语音识别与合成(Speech-to-Text/Text-to-Speech)\n - 视频识别与生成(Video Understanding/Generation)\n\n- **检索增强生成(RAG):**内置文本向量化(Embedding)和重排序(Rerank)能力,支持知识库检索增强生成(RAG)\n\n - 向量嵌入(Embedding)\n - 重排序(Rerank)\n - 支持Elasticsearch、Milvus主流向量数据库\n\n- **多智能体协同**:基于有向循环图实现多智能体协同工作流,支持串行、并行、路由、条件分支、裁判评估等丰富的流程编排能力,快速构建复杂多智能体系统\n\n \n\n - 多智能体编排工作流底层基于[bboss jobflow](https://esdoc.bbossgroups.com/#/jobworkflow)实现(一套数据交换作业编排工作流)\n\n- **多轮会话**:支持会话长短期记忆管理,会话消息智能压缩;支持多智能体协同通讯和信息共享;子智能体独立会话空间,彼此隔离,互不干扰;支持内存、关系数据库(MySQL、Oracle、达梦 DM、SQL Server、PostgreSQL、SQLite)、Clickhouse多种会话持久化机制,生产级推荐Clickhouse。\n\n- **Hitl(Human-in-the-Loop):**人工介入功能允许在智能体执行过程中,当遇到需要人工决策的关键节点时,暂停智能体执行并等待人工介入处理,使用参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-hitl\n\n- **智能体任务调度:灵活的智能体工作流任务**定时调度执行能力,可制定节假日或或者特定时间段忽略执行策略\n\n- **全链路监控:**智能体全链路 Trace 可观测性,覆盖 LLM 调用、工具执行、工作流编排;可基于智能体trace api,应用可自主记录智能体流程执行过程中的任何状态信息、数据信息和中间结果信息。\n\n### 支持的平台\n\n- DeepSeek\n- Kimi(Moonshot)\n- 智谱 AI(Zhipu)\n- 阿里百炼/通义千问(Qwen)\n- 字节豆包/火山引擎(Doubao)\n- 百度(Baidu)\n- 硅基流动(Siliconflow)\n- 九天(Jiutian)\n- MiniMax\n- 腾讯混元(TencentHY)\n- Xinference\n- OpenAI 兼容接口\n- ChatGPT\n\n## 二、环境准备\n\n### 2.1 Maven 依赖\n\n```xml\n<dependency>\n <groupId>com.bbossgroups</groupId>\n <artifactId>bboss-ai-flow</artifactId>\n <version>6.5.5</version>\n</dependency>\n```\n\n### 2.2 Gradle 依赖\n\n```groovy\nimplementation 'com.bbossgroups:bboss-ai-flow:6.5.5'\n```\n\n### 2.3 maas服务配置\n\n创建配置文件 `application-stream.properties`,配置各模型服务:使用时,需将apiKey替换为实际apiKey\n\n```properties\nhttp.poolNames = deepseek,qwenvlplus,volcengine,kimi,zhipu,minimax,hunyuan\n\n## DeepSeek 配置\ndeepseek.http.maxTotal = 200\ndeepseek.http.defaultMaxPerRoute = 200\ndeepseek.http.hosts=https://api.deepseek.com\ndeepseek.http.apiKeyId = sk-xxxxx\ndeepseek.http.modelType = deepseek\n\n## 阿里百炼通义千问配置\nqwenvlplus.http.maxTotal = 200\nqwenvlplus.http.defaultMaxPerRoute = 200\nqwenvlplus.http.hosts=https://dashscope.aliyuncs.com\nqwenvlplus.http.apiKeyId = sk-xxxxx\nqwenvlplus.http.modelType = qwen\n\n## 火山引擎豆包配置\nvolcengine.http.maxTotal = 200\nvolcengine.http.defaultMaxPerRoute = 200\nvolcengine.http.hosts=https://ark.cn-beijing.volces.com\nvolcengine.http.apiKeyId = xxxxx\nvolcengine.http.modelType = doubao\n\n## Kimi 配置\nkimi.http.maxTotal = 200\nkimi.http.defaultMaxPerRoute = 200\nkimi.http.hosts=https://api.moonshot.cn\nkimi.http.apiKeyId = sk-xxxxx\nkimi.http.modelType = kimi\n\n## 智谱配置\nzhipu.http.maxTotal = 200\nzhipu.http.defaultMaxPerRoute = 200\nzhipu.http.hosts=https://open.bigmodel.cn\nzhipu.http.apiKeyId = xxxxx\n\n## MiniMax 配置\nminimax.http.maxTotal = 200\nminimax.http.defaultMaxPerRoute = 200\nminimax.http.hosts=https://api.minimaxi.com\nminimax.http.apiKeyId = sk-xxxxx\nminimax.http.modelType = minimax\n\n## 腾讯混元配置\nhunyuan.http.maxTotal = 200\nhunyuan.http.defaultMaxPerRoute = 200\nhunyuan.http.hosts=https://api.hunyuan.cloud.tencent.com\nhunyuan.http.apiKeyId = sk-xxxxx\n```\n\n### 2.4 MCP 服务配置\n\n如需使用 MCP 工具服务,配置 `mcpserver.properties`:使用时,需将apiKey替换为实际apiKey\n\n```properties\nhttp.poolNames = aliyun,gaotie,amap,WebParser,visualops,12306,shuqi,feishumcp\n#分析文档,编写一篇简洁实用的bboss ai实用教程\n##aliyun mcp模型服务配置:在代码中引用服务的名称为aliyun\n# 服务连接池参数\naliyun.http.maxTotal = 200\naliyun.http.defaultMaxPerRoute = 200\n# aliyun模型服务地址\naliyun.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\naliyun.http.apiKeyId = sk-469f01dbb572310d6d6db4fb2f4\n# sse端点:高德地图用户key申请地址,https://console.amap.com/dev/key/app\naliyun.http.extendConfigs.sseendpoint = /api/v1/mcps/amap-maps/sse?key=793b8ba3f45a22fa74cba764a8\n\n##WebParser mcp模型服务配置:在代码中引用服务的名称为WebParser\n# 服务连接池参数\nWebParser.http.maxTotal = 200\nWebParser.http.defaultMaxPerRoute = 200\n# WebParser模型服务地址\nWebParser.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\nWebParser.http.apiKeyId = sk-469f01e6db310d6d6db4fb2f4\n# sse端点:高德地图用户key申请地址,https://console.amap.com/dev/key/app\nWebParser.http.extendConfigs.sseendpoint = /api/v1/mcps/WebParser/sse\n\n\n##gaode mcp模型服务配置:在代码中引用服务的名称为amap\n# 服务连接池参数\namap.http.maxTotal = 200\namap.http.defaultMaxPerRoute = 200\n# amap模型服务地址\namap.http.hosts=https://mcp.amap.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\n#amap.http.apiKeyId = sk-469f6db310d6d6db4fb2f4\n# sse端点:高德地图用户key申请地址,https://console.amap.com/dev/key/app\namap.http.extendConfigs.sseendpoint = /sse?key=793b8b9bab15a22fa74cba764a8\n\n\n##gaotie mcp模型服务配置:在代码中引用服务的名称为gaotie\n# 服务连接池参数\ngaotie.http.maxTotal = 200\ngaotie.http.defaultMaxPerRoute = 200\n# gaotie模型服务地址\ngaotie.http.hosts=127.0.0.1:8000\ngaotie.http.extendConfigs.sseendpoint = /sse\n\n##12306 mcp模型服务配置:在代码中引用服务的名称为12306\n# 服务连接池参数\n12306.http.maxTotal = 200\n12306.http.defaultMaxPerRoute = 200\n# 12306服务地址\n12306.http.hosts=https://dashscope.aliyuncs.com\n12306.http.apiKeyId = sk-469f01db6db310d6d6db4fb2f4\n12306.http.extendConfigs.sseendpoint = /api/v1/mcps/china-railway/sse\n\n##shuqi mcp模型服务配置:在代码中引用服务的名称为shuqi\n# 服务连接池参数\nshuqi.http.maxTotal = 200\nshuqi.http.defaultMaxPerRoute = 200\n# shuqi服务地址\nshuqi.http.hosts=https://dashscope.aliyuncs.com\nshuqi.http.apiKeyId = sk-469f01dbe6db310d6d6db4fb2f4\nshuqi.http.extendConfigs.streamableendpoint = /api/v1/mcps/market-cmapi00072981/mcp\n#shuqi.http.httpResponseInterceptors=org.frameworkset.spi.ai.HttpResponseInterceptorDemo\n\n##visualops mcp模型服务配置:在代码中引用服务的名称为visualops\n# 服务连接池参数\nvisualops.http.maxTotal = 200\nvisualops.http.defaultMaxPerRoute = 200\n# visualops服务地址\n\nvisualops.http.hosts=127.0.0.1:8080\nvisualops.http.apiKeyId = 17689048891086XsDsJVgwiQcmKhOdh23DX4NT\n#visualops.http.extendConfigs.sseendpoint = /mcp/sse.api\nvisualops.http.extendConfigs.streamableendpoint = /mcp/streamable.api\n\n##feishumcp mcp模型服务配置:在代码中引用服务的名称为feishumcp\n# 服务连接池参数\nfeishumcp.http.maxTotal = 200\nfeishumcp.http.defaultMaxPerRoute = 200\n# feishumcp服务地址\nfeishumcp.http.hosts=https://mcp.feishu.cn\nfeishumcp.http.extendConfigs.streamableendpoint = /mcp\n\n\n\n```\n\n---\n\n## 三、基础使用\n\n### 3.1 智能体消息类型\n\n系统内置消息类型定义:[SessionMessage](https://gitee.com/bboss/bboss-ai/blob/main/bboss-ai/src/main/java/org/frameworkset/spi/ai/store/SessionMessage.java)\n\n完整的消息类型对照表,可以参考:\n\n[16.2 消息类型对照表](#chapter16-2)\n\n#### 3.1.1 编码定义\n\n消息类型messageType,对应agent_session_message表中的messageType字段\n\n```java\n /**\n * 智能体输出消息:ASSISTANT中的一种\n */\n public static final String MESSAGE_TYPE_AGENT_RESULTMESSAGE = \"1\";\n /**\n * 智能体用户输入消息:提交给大模型或者其他多模态模型\n */\n public static final String MESSAGE_TYPE_USER_MESSAGE = \"2\";\n\n /**\n * 智能体辅助消息\n */\n public static final String MESSAGE_TYPE_ASSISTANT_MESSAGE = \"0\";\n\n\n /**\n * 智能体系统消息\n */\n public static final String MESSAGE_TYPE_SYSTEM_MESSAGE = \"3\";\n\n /**\n * 智能体辅助消息:工具调用结果消息,需要提交给大模型\n */\n public static final String MESSAGE_TYPE_TOOL_MESSAGE = \"4\";\n\n /**\n * 智能体跟踪消息\n */\n public static final String MESSAGE_TYPE_TRACE_MESSAGE = \"5\";\n\n /**\n * 智能体RAG知识消息\n */\n public static final String MESSAGE_TYPE_RAG_MESSAGE = \"6\";\n\n /**\n * 智能体拒答消息\n */\n public static final String MESSAGE_TYPE_REFUSE_MESSAGE = \"7\";\n /**\n * 智能体用户输入消息:包括用户输入的原始问题、用户上传文件、用户图片描述等\n */\n public static final String MESSAGE_TYPE_USER_INPUTMESSAGE = \"8\";\n\n /**\n * LLM输入消息\n */\n public static final String MESSAGE_TYPE_LLM_INPUTMESSAGE = \"9\";\n\n /**\n * LLM输出消息\n */\n public static final String MESSAGE_TYPE_LLM_OUTPUTMESSAGE = \"10\";\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_INPUTMESSAGE = \"11\";\n\n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_INPUTMESSAGE = \"12\";\n\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_OUTPUTMESSAGE = \"13\";\n \n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_OUTPUTMESSAGE = \"14\";\n /**\n * 智能体工具搜索匹配消息\n */\n public static final String MESSAGE_TYPE_TOOLSEARCH_MESSAGE = \"15\";\n\n\n /**\n * 智能体mcp服务调用消息\n */\n public static final String MESSAGE_TYPE_MCPCALL_MESSAGE = \"16\";\n\n /**\n * 智能体工具服务调用消息\n */\n public static final String MESSAGE_TYPE_TOOLCALL_MESSAGE = \"17\";\n\n\n /**\n * 智能体工具服务调用消息:其他消息类型,定义的消息名称(role)没有设置对应的编码,返回18\n * 需要在AgentMessageTypeConvertor中建立映射关系\n */\n public static final String MESSAGE_TYPE_OTHER_MESSAGE = \"18\";\n\t\n\t/**\n\t * 智能体Skill调用消息\n\t */\n\tpublic static final String MESSAGE_TYPE_SKILLCALL_MESSAGE = \"19\";\n\t\n\t/**\n\t * 智能体人工介入消息:大模型提供输入内容,需要人工介入处理\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_MESSAGE = \"20\";\n\t\n\t/**\n\t * 智能体人工介入处理消息:人工介入处理完成后,返回给大模型的消息和数据内容\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_HANDLE_MESSAGE = \"21\";\n\n\n```\n\n#### 3.1.2 名称定义\n\n消息类型messageType对应的角色名称:\n\n```java\n\n /**\n * 智能体用户输入消息:包括用户输入的原始问题、用户上传文件、用户图片描述等\n */\n public static final String MESSAGE_TYPE_USER_INPUTMESSAGE_NAME = \"userinput\";\n\n /**\n * LLM输入消息\n */\n public static final String MESSAGE_TYPE_LLM_INPUTMESSAGE_NAME = \"llminput\";\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_INPUTMESSAGE_NAME = \"embeddinginput\";\n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_INPUTMESSAGE_NAME = \"rerankinput\";\n\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_OUTPUTMESSAGE_NAME = \"embeddingoutput\";\n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_OUTPUTMESSAGE_NAME = \"rerankoutput\";\n \n /**\n * LLM输出消息\n */\n public static final String MESSAGE_TYPE_LLM_OUTPUTMESSAGE_NAME = \"llmoutput\";\n /**\n * 智能体输出消息:ASSISTANT中的一种\n */\n public static final String MESSAGE_TYPE_AGENT_RESULTMESSAGE_NAME = \"agentresult\";\n /**\n * 智能体用户输入消息\n */\n public static final String MESSAGE_TYPE_USER_MESSAGE_NAME = \"user\";\n\n /**\n * 智能体辅助消息\n */\n public static final String MESSAGE_TYPE_ASSISTANT_MESSAGE_NAME = \"assistant\";\n\n /**\n * 智能体辅助消息:工具调用结果消息,需要提交给大模型\n */\n public static final String MESSAGE_TYPE_TOOL_MESSAGE_NAME = \"tool\";\n\n\n /**\n * 智能体工具搜索匹配消息\n */\n public static final String MESSAGE_TYPE_TOOLSEARCH_MESSAGE_NAME = \"toolsearch\";\n\n\n /**\n * 智能体系统消息\n */\n public static final String MESSAGE_TYPE_SYSTEM_MESSAGE_NAME = \"system\";\n\n\n\n /**\n * 智能体跟踪消息\n */\n public static final String MESSAGE_TYPE_TRACE_MESSAGE_NAME = \"trace\";\n\n /**\n * 智能体RAG知识消息\n */\n public static final String MESSAGE_TYPE_RAG_MESSAGE_NAME = \"rag\";\n\n /**\n * 智能体拒答消息\n */\n public static final String MESSAGE_TYPE_REFUSE_MESSAGE_NAME = \"refuse\";\n\n /**\n * 智能体mcp服务调用消息名称\n */\n public static final String MESSAGE_TYPE_MCPCALL_MESSAGE_NAME = \"mcpcall\";\n\n /**\n * 智能体工具服务调用消息\n */\n public static final String MESSAGE_TYPE_TOOLCALL_MESSAGE_NAME = \"toolcall\";\n\t\n\t/**\n\t * 智能体Skill调用消息\n\t */\n\tpublic static final String MESSAGE_TYPE_SKILLCALL_MESSAGE_NAME = \"skillcall\";\n\t\n\t/**\n\t * 智能体人工介入消息\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_MESSAGE_NAME = \"hitltask\";\n\t\n\t\n\t/**\n\t * 智能体人工介入处理消息\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_HANDLE_MESSAGE_NAME = \"hitlhandle\";\n```\n\n#### 3.1.3 扩展消息类型\n\nAgentMessageTypeConvertor提供以上内置角色名称到消息类型的转换,如果用户需要扩展自己的消息角色和编码,可以继承AgentMessageTypeConvertor类并重写convertMessageType方法,0-100为系统内置编码,如果用户自定义编码请从101开始,示例如下:\n\n```java\npublic class CustomAgentMessageTypeConvertor extends AgentMessageTypeConvertor {\n /**\n * 将角色转换为消息类型messageType,对应agent_session_message表中的messageType字段 \n *\n * @param role\n * @return\n */\n @Override\n public String convertMessageType(String role) {\n if(\"custom\".equals(role)){\n return \"101\";\n }\n return super.convertMessageType(role);\n }\n}\n```\n\n使用自定义转换器:\n\n```java\nStoreContext storeContext = new StoreContext();\nstoreContext.setAgentMessageTypeConvertor(new CustomAgentMessageTypeConvertor());\n```\n\n#### 3.1.3 使用场景\n\n消息类型使用场景:在智能体编排流程中设置消息角色类型\n\n```java\n TraceMessage traceMessage = new TraceMessage(); \n //记录用户输入的原始问题\n traceMessage.setMessage(Map.of(\"question\", question,\"role\",SessionMessage.MESSAGE_TYPE_USERINPUTMESSAGE_NAME));\n //其他用户上传的附件材料信息可以放到metaData中,也可以直接放到上面的消息中\n\n traceMessage.setStartTime(System.currentTimeMillis());\n\n planAgent.recordTraceMessage(traceMessage);\n```\n\n#### 3.1.4 ServerEvent结构\n\n```java\ncontentType:主要用来区分答案正文内容和思维链内容,0表示答案内容,1表示思维链内容, 2 表示工具调用,3 表示mcp服务调用,5 表示监控对象,默认值为0\n\n type 字段:标记消息报文类型 \n /**\n * 数据报文类型:0 数据消息,1表示异常消息,2 trace信息,traceId 3 拒答消息 5 知识库资料消息 6 步骤消息\n * 默认值为0 \n */\ntype = TYPE_DATA;\n数据报文类型常量说明\n /**\n * type:数据消息\n */\n public static final int TYPE_DATA = 0;\n /**\n * type:异常消息\n */\n public static final int TYPE_ERROR = 1;\n\n /**\n * type:trace信息,traceId\n */\n public static final int TYPE_TRACE = 2;\n /**\n * type:拒绝消息:\n */\n public static final int TYPE_REFUSAL = 3;\n \n /**\n * type:知识库资料消息:\n */\n public static final int TYPE_RAG_KNOWLEDGE = 5;\n\n /**\n * type:步骤消息:\n */\n public static final int TYPE_STEP = 6;\n```\n\n#### 3.1.5 属性说明\n\n| 参数 | 说明 |\n| -------------- | ---------------- |\n| `agentId` | 智能体唯一标识 |\n| `agentName` | 智能体名称 |\n| `prompt` | 用户提示词 |\n| `systemPrompt` | 系统提示词 |\n| `model` | 模型名称 |\n| `maas` | MaaS平台服务名 |\n| `stream` | 是否开启流式响应 |\n| `thinking` | 是否开启思考过程 |\n\n---\n\n### \n\n### 3.2 初始化配置\n\n在应用启动时加载配置文件:\n\n```java\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\n\npublic class Application {\n public void init() {\n // 加载模型服务配置文件\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n \n // 加载 MCP 服务配置文件\n HttpRequestProxy.startHttpPools(\"mcpserver.properties\");\n }\n}\n```\n\n### 3.3 同步聊天(非流式)\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.ChatAgentMessage;\nimport org.frameworkset.spi.ai.model.ServerEvent;\n\npublic class ChatExample {\n public static void main(String[] args) {\n // 初始化配置\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n \n // 创建消息对象\n ChatAgentMessage chatAgentMessage = new ChatAgentMessage();\n chatAgentMessage.setModel(\"deepseek-chat\"); // 模型名称\n chatAgentMessage.setPrompt(\"介绍一下 Java Reactor 编程模式\");\n chatAgentMessage.setSystemPrompt(\"你是一个编程大师\");\n chatAgentMessage.setTemperature(0.7); // 温度参数\n chatAgentMessage.setMaxTokens(8192); // 最大输出 Token\n \n // 创建 AIAgent 并调用\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.chat(\"deepseek\", chatAgentMessage);\n \n // 输出结果\n System.out.println(response.getData());\n }\n}\n```\n\n### 3.4 流式聊天\n\n```java\nimport reactor.core.publisher.Flux;\nimport java.util.concurrent.CountDownLatch;\n\npublic class StreamChatExample {\n public static void main(String[] args) throws InterruptedException {\n // 初始化配置\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n // 创建消息对象,设置 stream=true\n ChatAgentMessage chatAgentMessage = new ChatAgentMessage();\n chatAgentMessage.setModel(\"deepseek-chat\");\n chatAgentMessage.setPrompt(\"介绍一下 Java Reactor 编程模式\");\n chatAgentMessage.setStream(true);\n chatAgentMessage.setTemperature(0.7);\n chatAgentMessage.setMaxTokens(8192);\n\n CountDownLatch countDownLatch = new CountDownLatch(1);\n\n AIAgent aiAgent = new AIAgent();\n Flux<ServerEvent> flux = aiAgent.streamChat(\"deepseek\", chatAgentMessage);\n\n flux.doOnSubscribe(subscription -> System.out.println(\"开始订阅流...\"))\n .doOnNext(chunk -> {\n if (chunk.getData() != null) {\n System.out.print(chunk.getData());\n }\n })\n .doOnComplete(() -> {\n System.out.println(\"\\n=== 流完成 ===\");\n countDownLatch.countDown();\n })\n .doOnError(error -> {\n error.printStackTrace();\n countDownLatch.countDown();\n })\n .subscribe();\n\n // 等待异步完成\n countDownLatch.await();\n }\n}\n```\n\n### 3.5 多轮会话\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class SessionChatExample {\n // 静态会话记忆列表\n static List<Map<String, Object>> sessionMemory = new ArrayList<>();\n\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n AIAgent aiAgent = new AIAgent();\n\n // 第一轮对话\n ChatAgentMessage message1 = new ChatAgentMessage();\n message1.setModel(\"deepseek-chat\");\n message1.setPrompt(\"我叫张三\");\n message1.setSessionMemory(sessionMemory); // 传入会话记忆\n message1.setSessionSize(50); // 保留最近50条消息\n\n ServerEvent response1 = aiAgent.chat(\"deepseek\", message1);\n System.out.println(\"AI: \" + response1.getData());\n\n // 第二轮对话,AI 会记住用户名字\n ChatAgentMessage message2 = new ChatAgentMessage();\n message2.setModel(\"deepseek-chat\");\n message2.setPrompt(\"我叫什么名字?\");\n message2.setSessionMemory(sessionMemory);\n\n ServerEvent response2 = aiAgent.chat(\"deepseek\", message2);\n System.out.println(\"AI: \" + response2.getData());\n\n // 重置会话\n sessionMemory.clear();\n }\n}\n```\n\n---\n\n多智能体协同编排,访问章节:[多智能体协同编排](https://esdoc.bbossgroups.com/#/bboss-ai?id=%e5%8d%81%e5%9b%9b%e3%80%81%e5%a4%9a%e6%99%ba%e8%83%bd%e4%bd%93%e7%bc%96%e6%8e%92%e5%b7%a5%e4%bd%9c%e6%b5%81)\n\n### 3.6 失败重试\n\n如果模型服务不是太稳定,则需要设置重试机制,通过以下参数设置重试机制:\n\n**retry** 设置重试次数,大于0时,当调用大模型失败时,会自动重试,否则不重试,默认值0\n\n**retryInterval** 每次重试时,需等待特定时间后再重试,单位毫秒,大于0时才进行等待,默认值:500毫秒\n\n```java\nChatAgentMessage chatAgentMessage = new ChatAgentMessage();\nchatAgentMessage.setModel(\"deepseek-v4-pro\")\n .setStream( true)\n .setRetry(3).setRetryInterval(1000L)\n .setMaas(\"deepseek\").setTemperature(0.3)\n .setPrompt(question);\n```\n\n### 3.7 与spring boot 集成\n\n本节介绍bboss ai与spring boot集成,实现智能体问答功能。\n\n参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-qa\n\n## 四、图片识别(视觉大模型)\n\n### 4.1 单张图片识别\n\n```java\nimport org.frameworkset.spi.ai.model.ImageVLAgentMessage;\nimport com.frameworkset.util.FileUtil;\n\npublic class ImageRecognitionExample {\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n ImageVLAgentMessage imageMsg = new ImageVLAgentMessage();\n imageMsg.setModel(\"qwen3-vl-plus\");\n imageMsg.setPrompt(\"介绍图片内容\");\n\n // 方式1:使用图片 URL\n imageMsg.addImageUrl(\"https://example.com/image.jpg\");\n\n // 方式2:使用 Base64 编码\n String base64 = FileUtil.getBase64Content(\"/path/to/image.jpg\");\n imageMsg.addImageUrl(base64);\n\n // 可选参数\n imageMsg.addParameter(\"enable_thinking\", true); // 开启思考过程\n imageMsg.addParameter(\"thinking_budget\", 81920); // 推理 Token 数\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.imageParser(\"qwenvlplus\", imageMsg);\n\n System.out.println(response.getData());\n }\n}\n```\n\n### 4.2 流式图片识别\n\n```java\npublic static void streamImageRecognition() throws InterruptedException {\n ImageVLAgentMessage imageMsg = new ImageVLAgentMessage();\n imageMsg.setModel(\"qwen3-vl-plus\");\n imageMsg.setPrompt(\"介绍图片内容\");\n imageMsg.addImageUrl(\"https://example.com/image.jpg\");\n imageMsg.setStream(true);\n\n CountDownLatch latch = new CountDownLatch(1);\n\n AIAgent aiAgent = new AIAgent();\n Flux<ServerEvent> flux = aiAgent.streamImageParser(\"qwenvlplus\", imageMsg);\n\n flux.doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n}\n```\n\n### 4.3 多张图片对比\n\n```java\npublic static void compareImages() {\n ImageVLAgentMessage imageMsg = new ImageVLAgentMessage();\n imageMsg.setModel(\"qwen3-vl-plus\");\n imageMsg.setPrompt(\"对比两张图片的相似度,以 JSON 格式返回结果\");\n\n imageMsg.addImageUrl(\"https://example.com/image1.jpg\");\n imageMsg.addImageUrl(\"https://example.com/image2.jpg\");\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.imageParser(\"qwenvlplus\", imageMsg);\n\n System.out.println(response.getData());\n}\n```\n\n---\n\n## 五、图片生成\n\n### 5.1 文本生成图片\n\n```java\nimport org.frameworkset.spi.ai.model.ImageAgentMessage;\nimport org.frameworkset.spi.ai.model.ImageEvent;\n\npublic class ImageGenerationExample {\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n ImageAgentMessage request = new ImageAgentMessage();\n request.setPrompt(\"一只可爱的橘猫,坐在窗台上晒太阳,油画风格\");\n request.setModel(\"qwen-image-plus\");\n\n // 可选参数\n request.addParameter(\"size\", \"1328*1328\"); // 图片尺寸\n request.addParameter(\"watermark\", false); // 是否添加水印\n request.addParameter(\"prompt_extend\", true); // 提示词扩展\n\n // 设置图片保存路径\n request.setStoreFilePathFunction(imageUrl -> {\n return \"image/\" + System.currentTimeMillis() + \".jpg\";\n });\n\n AIAgent aiAgent = new AIAgent();\n ImageEvent result = aiAgent.genImage(\"qwenvlplus\", request);\n\n System.out.println(\"生成的图片 URL: \" + result.getGenImageUrl());\n System.out.println(\"本地保存路径: \" + result.getImageUrl());\n }\n}\n```\n\n### 5.2 火山引擎豆包图片生成\n\n```java\npublic static void doubaoImageGen() {\n ImageAgentMessage request = new ImageAgentMessage();\n request.setPrompt(\"一只可爱的小狗,卡通风格\");\n request.setModel(\"doubao-seedream-5-0-260128\");\n\n // 豆包特有参数\n request.addParameter(\"response_format\", \"url\");\n request.addParameter(\"size\", \"2k\");\n request.addParameter(\"watermark\", true);\n request.addParameter(\"sequential_image_generation\", \"auto\"); // 多图生成\n request.addMapParameter(\"sequential_image_generation_options\",\n \"max_images\", 3);\n\n AIAgent aiAgent = new AIAgent();\n ImageEvent result = aiAgent.genImage(\"volcengine\", request);\n\n System.out.println(\"生成图片数量: \" + result.getImageUrls().size());\n}\n```\n\n### 5.3 图文生图(修图)\n\n```java\nString selectedModel = (String)questions.get(\"selectedModel\");\nboolean generateMultipleImages = questions != null?(boolean)questions.get(\"generateMultipleImages\"):false;\nString message = null;\nmessage = questions != null?(String)questions.get(\"message\"):null;\n if(SimpleStringUtil.isEmpty( message)){\nmessage = \"生成一颗桂花树\";\n }\nImageAgentMessage request = new ImageAgentMessage();\n request.setStoreFilePathFunction(new StoreFilePathFunction() {\n @Override\n public String getStoreFilePath(String imageUrl) {\n return \"image/\"+SimpleStringUtil.getUUID32() +\".jpg\";\n }\n});\n\nList<String> imagesBase64 = (List)questions.get(\"imagesBase64\");\nString imageUrl = (String)questions.get(\"imageUrl\");\n if(imageUrl != null) {\nimageUrl = imageUrl.trim();\n }\n\n\n\n\n if(SimpleStringUtil.isNotEmpty(imageUrl)) {\n request.addImageUrl(imageUrl);\n }\n if(SimpleStringUtil.isNotEmpty(imagesBase64)) {\n for(String tmp:imagesBase64) {\n request.addImageUrl(tmp);\n// Map<String, Object> requestMap = new HashMap<>();\n// requestMap.put(\"model\", \"LLMImage2Text\");\n// requestMap.put(\"image\",tmp);\n// requestMap.put(\"prompt\", message);\n// requestMap.put(\"stream\", true);\n// String rsp = HttpRequestProxy.httpPostforString(\"jiutian\", \"/largemodel/moma/api/v3/image/text\", requestMap);\n// logger.info(rsp);\n }\n\n\n }\n request.setPrompt( message);\nImageEvent data = null;\nAIAgent aiAgent = new AIAgent();\n// String completionsUrl = null;\n if(selectedModel.equals(\"volcengine\")){\n //字节火山引擎\n request.setModel( \"doubao-seedream-4-5-251128\");\n if(!generateMultipleImages) {\n request.addParameter(\"sequential_image_generation\", \"disabled\");//生成单图\n }\n else {\n request.addParameter(\"sequential_image_generation\", \"auto\");//生成多图\n request.addMapParameter(\"sequential_image_generation_options\",\"max_images\",5);\n }\n request.addParameter(\"response_format\", \"url\");\n request.addParameter(\"size\", \"2k\");\n request.addParameter(\"watermark\", true);\n// completionsUrl = \"/api/v3/images/generations\";\n }\n else if(selectedModel.equals(\"jiutian\")){\n //字节火山引擎\n request.setModel( \"cntxt2image\");\n// completionsUrl = \"/largemodel/moma/api/v3/images/generations\";\n }\n else{\n\n //阿里百炼\n //通过在http.modelType指定全局模型适配器类型,亦可以在ImageAgentMessage对象设置请求级别modelType模型适配器类型(优先级高于全局模型适配器类型)\n request.setModel( \"qwen-image-edit-max-2026-01-16\");\n request.addParameter(\"n\",5);\n request.addParameter(\"prompt_extend\",true);\n request.addParameter(\"watermark\",false);\n request.addParameter(\"size\",\"1536*1024\");\n///api/v1/services/aigc/multimodal-generation/generation\n// completionsUrl = \"/api/v1/services/aigc/multimodal-generation/generation\";\n\n }\nImageEvent imagedata = aiAgent.genImage(selectedModel,request);\nSystem.out.println(\"生成图片数量: \" + result.getImageUrls().size());\n```\n\n---\n\n## 六、语音识别(STT)\n\n```java\nimport org.frameworkset.spi.ai.model.AudioSTTAgentMessage;\n\npublic class SpeechRecognitionExample {\n public static void main(String[] args) throws IOException {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n AudioSTTAgentMessage audioMsg = new AudioSTTAgentMessage();\n audioMsg.setModel(\"qwen3-asr-flash\");\n audioMsg.setPrompt(\"介绍音频内容\");\n\n // 方式1:设置音频文件\n File audioFile = new File(\"/path/to/audio.wav\");\n audioMsg.setAudio(audioFile);\n audioMsg.setContentType(\"audio/wav\");\n\n // 方式2:设置音频 URL\n // audioMsg.setAudio(\"https://example.com/audio.mp3\");\n\n // 可选参数\n audioMsg.addMapParameter(\"asr_options\", \"enable_itn\", true);\n audioMsg.addParameter(\"incremental_output\", true);\n audioMsg.setResultFormat(\"message\");\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent result = aiAgent.audioParser(\"qwenvlplus\", audioMsg);\n\n System.out.println(\"识别结果: \" + result.getData());\n }\n}\n```\n\n---\n\n## 七、语音合成(TTS)\n\n### 7.1 同步语音合成\n\n通义千问语音生成:\n\n```java\nimport org.frameworkset.spi.ai.model.AudioAgentMessage;\nimport org.frameworkset.spi.ai.model.AudioEvent;\n\npublic class TextToSpeechExample {\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n AudioAgentMessage audioMsg = new AudioAgentMessage();\n audioMsg.setModel(\"qwen3-tts-flash\");\n audioMsg.setPrompt(\"诗歌朗诵:床前明月光,疑似地上霜。\");\n\n // 可选参数\n audioMsg.addParameter(\"voice\", \"Cherry\"); // 音色\n audioMsg.addParameter(\"language_type\", \"Chinese\"); // 语言\n\n // 设置保存路径,通过StoreFilePathFunction保存音频文件到本地\n audioMsg.setStoreFilePathFunction(url -> {\n return \"audio/\" + System.currentTimeMillis() + \".wav\";\n });\n\n AIAgent aiAgent = new AIAgent();\n AudioEvent result = aiAgent.genAudio(\"qwenvlplus\", audioMsg);\n\n System.out.println(\"音频文件路径: \" + result.getAudioUrl());\n }\n}\n```\n\n智谱语音生成:\n\n```java\n AudioAgentMessage audioMsg = new AudioAgentMessage();\n //https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E6%96%87%E6%9C%AC%E8%BD%AC%E8%AF%AD%E9%9F%B3\n \n \t\t\taudioMsg.setModel(\"glm-tts\");\n \taudioMsg.setPrompt(\"诗歌朗诵:床前明月光,疑似地上霜。\");\n audioAgentMessage.addParameter(\"voice\", \"female\")\n .addParameter(\"response_format\", \"wav\")\n .addParameter(\"speed\", 1.0)\n .addParameter(\"volume\", 1.0) \n ;\n // 设置保存路径,智谱语音模型默认返回对应音频文件格式的base64字符串。\n audioMsg.setStoreFilePathFunction(new ReponseStoreFilePathFunction() {\n @Override\n public String getStoreFilePath(String imageUrl) {\n return \"audio/\"+SimpleStringUtil.getUUID32() +\".wav\";\n }\n }); \n \n \n AIAgent aiAgent = new AIAgent();\n AudioEvent audioEvent = aiAgent.genAudio(selectedModel,audioAgentMessage,storeFilePathFunction);\n return audioEvent;\n```\n\nStoreFilePathFunction和ReponseStoreFilePathFunction区别:\n\nStoreFilePathFunction:当模型将生成的语音通过url方式返回时,采用StoreFilePathFunction保存到本地音频文件\n\nReponseStoreFilePathFunction:当模型将生成的语音通过base64码方式返回时,采用ReponseStoreFilePathFunction直接将base64编码音频内容保存到本地音频文件\n\n### 7.2 流式语音合成\n\n```java\npublic static void streamTextToSpeech() throws InterruptedException {\n AudioAgentMessage audioMsg = new AudioAgentMessage();\n audioMsg.setModel(\"qwen3-tts-flash\");\n audioMsg.setPrompt(\"欢迎使用 bboss AI 语音合成服务\");\n audioMsg.addParameter(\"voice\", \"Cherry\");\n audioMsg.setStream(true);\n\n CountDownLatch latch = new CountDownLatch(1);\n\n AIAgent aiAgent = new AIAgent();\n Flux<ServerEvent> flux = aiAgent.streamAudioGen(\"qwenvlplus\", audioMsg);\n\n flux.doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n // 处理音频数据块\n byte[] audioData = Base64.getDecoder().decode(chunk.getData());\n // 播放或保存音频数据\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n}\n```\n\n---\n\n## 八、工具调用(Function Calling)\n\n基于bboss发布工具函数参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-toolannotation\n\n高效工具检索功能参考使用文档:https://esdoc.bbossgroups.com/#/bboss-ai-toolsearcher\n\n### 8.1 本地工具调用\n\n```java\nimport org.frameworkset.spi.ai.model.FunctionToolDefine;\nimport org.frameworkset.spi.ai.model.ToolFunctionCall;\n\npublic class ToolCallExample {\n public static void main(String[] args) {\n ChatAgentMessage chatMsg = new ChatAgentMessage();\n chatMsg.setModel(\"deepseek-chat\");\n chatMsg.setPrompt(\"查询杭州的天气,并给出穿衣建议\");\n\n // 定义天气查询工具\n FunctionToolDefine weatherTool = new FunctionToolDefine();\n weatherTool.funtionName2ndDescription(\"weather_info_query\",\"天气查询服务,根据城市查询当地温度和天气信息\")\n// .putParametersType(\"object\")\n .requiredParameters(\"location\")\n .addSubParameter(\"params\",\"location\",\"string\",\"城市或者地州, 例如:上海市\")\n .setFunctionCall(new ToolFunctionCall() );\n\n chatMsg.registTool(weatherTool);\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.chat(\"deepseek\", chatMsg);\n System.out.println(response.getData());\n }\n\n\n}\n```\n\nToolFunctionCall工具调用实现:\n\n```java\nimport org.frameworkset.spi.ai.model.FunctionCall;\nimport org.frameworkset.spi.ai.model.FunctionCallException;\nimport org.frameworkset.spi.ai.model.FunctionTool;\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\n\nimport java.util.List;\nimport java.util.Map;\n\n\npublic class ToolFunctionCall implements FunctionCall {\n private String toolDatasource = \"tool\";\n\n public ToolFunctionCall(String toolDatasource) {\n this.toolDatasource = toolDatasource;\n }\n public ToolFunctionCall( ) {\n }\n\n\n @Override\n public Object call(FunctionTool functionTool) throws FunctionCallException {\n Map response = HttpRequestProxy.sendJsonBody(toolDatasource,functionTool.getArguments(), \"/openapi/\"+functionTool.getFunctionName() + \".api\", Map.class);\n List data = (List)response.get(\"data\");\n if(data == null || data.size() == 0){\n return \"没有找到对应数据\";\n }\n return data;\n// return \"20.22℃\";\n }\n}\n```\n\n\n\n### 8.2 远程工具调用\n\n```java\nchatMsg.setToolsRegist(new ToolsRegist() {\n @Override\n public List<FunctionToolDefine> registTools() {\n // 从远程服务获取工具定义\n Map<String, Object> params = new HashMap<>();\n params.put(\"apiKey\", \"your-api-key\");\n return HttpRequestProxy.sendJsonBodyForList(\"tool\", params,\n \"/function/tools.api\", FunctionToolDefine.class);\n }\n\n @Override\n public FunctionCall getFunctionCall(String functionName) {\n // 返回远程工具调用器\n return new ToolFunctionCall(\"tool\");\n }\n});\n```\n\n---\n\n### 8.3 多轮工具调用(Loop Tool Call)\n\n对于需要多步骤执行的复杂任务,智能体可能需要**多次调用工具**才能完成。bboss-ai 内置了循环工具调用机制,允许模型根据前序工具执行结果,自动决策是否继续调用下一个工具。\n\n**核心 API:**\n\n| 方法 | 说明 |\n| ---------------------------------------- | ----------------------------------- |\n| `AIAgent.setEnableLoopToolCall(boolean)` | 启用/禁用循环工具调用,默认 `false` |\n| `AIAgent.setMaxLoopToolCalls(int)` | 设置最大循环次数,防止无限循环 |\n\n使用示例:\n\n1)提示词资源文件中定义多轮任务\n\n```markdown\n用户要求:#[input.query],请依次执行以下命令:\n1.获取OS版本信息\n2.获取CPU信息\n3.打印OS和CPU信息\n4.查找占用808端口的进程,如果存在对应进程,则关闭进程。注意:端口是808,不要查找其他端口\n6.打印端口进程信息和端口关闭结果\n```\n2)智能体执行多轮任务\n\n1. 加载多任务提示词工程\n2. 启用智能体多次调用工具机制(默认关闭)\n3. 注册智能体完成任务所需的工具清单\n```java\n AINodeAgent scan2ndClosePortProcessAgent = new AINodeAgent(\"#[prompts/rag-looptoolcall-prompt.txt,type=resource]\")\n .setSystemPrompt(\"你是一个专家,可以根据用户要求获取系统信息,生成符合要求的、完整的、可执行的shell脚本\" +\n \",并将生成的脚本交由工具执行,输出执行结果。注意事项:通过Java Process调用cmd或者sh来执行脚本,确保脚本在目标操作系统上能够正常运行。\")\n .setAgentId(\"scan2ndClosePortProcessAgent\").setAgentName(\"扫描并关闭端口进程\");\n scan2ndClosePortProcessAgent.setEnableLoopToolCall(true);//启用智能体多次调用工具机制\n scan2ndClosePortProcessAgent.setMaxLoopToolCalls(100);\n //注册获取当前操作系统OS信息工具:框架内置工具\n scan2ndClosePortProcessAgent.registBeanTool(new GetOSFunctionTool(60));\n// //注册脚本执行工具,会根据获取到的OS信息,生成对应的OS环境命令行脚本进行执行:框架内置工具\n scan2ndClosePortProcessAgent.registBeanTool(new CLIShellFunctionTool(60));\n```\n\n\n\n### 8.4 内置工具\n\nbboss提供了一系列内置工具,比如脚本执行、文件操作、获取系统信息、代码执行等,可以访问以下文档了解内置工具的介绍和使用方法:\n\nhttps://esdoc.bbossgroups.com/#/bboss-ai-innertools\n\n## 九、MCP 服务集成\n\n基于bboss发布mcp服务参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-mcpbean\n\n基于bboss发布mcp服务并与spring boot集成参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-springboot\n\n### 9.1 使用 MCP 工具\n\n```java\nimport org.frameworkset.spi.ai.mcp.tools.MCPToolsRegist;\n\npublic class MCPExample {\n public static void main(String[] args) {\n ChatAgentMessage chatMsg = new ChatAgentMessage();\n chatMsg.setModel(\"deepseek-chat\");\n chatMsg.setPrompt(\"帮我查一下明天北京到上海的高铁\");\n\n // 注册 12306高铁MCP 工具服务,\n chatMsg.setToolsRegist(new MCPToolsRegist(\"12306\")); // 对应配置文件中的服务名\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.chat(\"deepseek\", chatMsg);\n System.out.println(response.getData());\n }\n}\n```\n\n### 9.2 流式 MCP 调用\n\n```java\npublic static void streamChatWithMcpTools(String maas, String mcpServer,\n String model, String prompt)\n throws InterruptedException {\n\n List<Map<String, Object>> session = new ArrayList<>();\n ChatAgentMessage chatMsg = new ChatAgentMessage()\n .setPrompt(prompt)\n .setSessionSize(50)\n .setSessionMemory(session)\n .setModel(model)\n .setStream(true)\n .setMaxTokens(4096);\n\n chatMsg.setToolsRegist(new MCPToolsRegist(mcpServer));\n\n CountDownLatch latch = new CountDownLatch(1);\n AIAgent aiAgent = new AIAgent();\n\n aiAgent.streamChat(maas, chatMsg)\n .doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n } else if (chunk.isToolCallsType()) {\n System.out.println(\"\\n开始执行工具...\");\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n}\n```\n\n---\n\n### 9.3 飞书MCP服务集成调用\n\n飞书MCP服务实现与标准MCP服务有所差别:需要通过http header设置临时AccessToken和工具清单配置\n\n飞书开发者调用远程 MCP 服务文档:\n\nhttps://open.feishu.cn/document/mcp_open_tools/developers-call-remote-mcp-server\n\n下面是bboss对接和使用飞书Mcp服务的案例代码:feishumcp server配置,参考前文的mcp服务配置,基于streamable 访问飞书Mcp服务\n\n```java\n BaseFeishuConfig baseFeishuConfig = new BaseFeishuConfig();\n// 设置bboss应用id和应用token,用于申请访问飞书Mcp服务的AccessToken\n baseFeishuConfig.setFeishuAppId(\"cli_a9d43b8f89cd0\")\n .setFeishAppSecret(\"gIhy0EbVfgQGlpNBN8rqMKMnYCJs\");\n//定义用于获取飞书Mcp服务认证Token的http数据源:feishu,用例访问开发平台获取飞书MCP服务认证Token\n baseFeishuConfig.addHttpConfig(\"http.poolNames\", \"feishu\")\n .addHttpConfig(\"feishu.http.hosts\", \"https://open.feishu.cn\")\n .addHttpConfig(\"feishu.http.maxTotal\", 100)\n .addHttpConfig(\"feishu.http.defaultMaxPerRoute\", 100)\n .addHttpConfig(\"feishu.http.authorTokenFunction\", \"org.frameworkset.spi.ai.mcp.feishu.FeishuMCPAuthorTokenFunction\")\n feishumcp.http.authorTokenFunction = \n// 25分钟自动刷新token\n// tenant_access_token 的最大有效期是 2 小时。\n// 剩余有效期小于 30 分钟时,调用本接口会返回一个新的 tenant_access_token,这会同时存在两个有效的 tenant_access_token。\n// 剩余有效期大于等于 30 分钟时,调用本接口会返回原有的 tenant_access_token\nfeishumcp.http.authorTokenExpiredTime = 1500000\n \n .addHttpConfig(\"feishu.http.authorTokenExpiredTime\", 1500000)\n .setMcpTools(\"search-user,get-user,fetch-file,search-doc,create-doc,fetch-doc,update-doc,list-docs,get-comments,add-comments\");//需要设置飞书Mcp工具清单,否则大模型无法识别Mcp工具并调用\n;\n chatAgentMessage.setToolsRegist(new FeishuMcpRegist(\"feishumcp\",baseFeishuConfig));\n\nCountDownLatch latch = new CountDownLatch(1);\nAIAgent aiAgent = new AIAgent();\n\n aiAgent.streamChat(maas, chatMsg)\n .doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n } else if (chunk.isToolCallsType()) {\n System.out.println(\"\\n开始执行工具...\");\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n```\n\n### 9.4 MCP 客户端\n\n`MCPClient` 实现了 Model Context Protocol 协议客户端,支持两种传输模式:\n\n#### 9.4.1 **核心功能**\n\n- SSE(Server-Sent Events)连接管理(`MCPSSEClient`)\n- Streamable HTTP 连接管理(`MCPStreamableClient`)\n- 会话生命周期管理(initialize、notifications/initialized)\n- 工具列表获取(tools/list)\n- 工具调用(tools/call)\n- 飞书 MCP 集成(`FeishuMCPClient`、`FeishuMCPStreamableClient`)\n\n#### 9.4.2 **工作流程**\n\n1. 通过 SSE 端点或 Streamable HTTP 端点建立连接\n2. 接收 endpoint 事件获取 messagePath 和 sessionId\n3. 发送 initialize 请求进行协议初始化\n4. 发送 notifications/initialized 通知服务端\n5. 正常进行工具列表查询和调用\n\n#### **9.4.3 Spring Boot 客户端集成示例**\n\n```java\n// 1. 配置 mcpserver.properties\n// http.poolNames = mcp_server\n// mcp_server.http.hosts = 127.0.0.1:8889\n// mcp_server.http.apiKeyId = 123456\n// mcp_server.http.extendConfigs.streamableendpoint = /ecop-biz-srv/mcp/streamable\n\n// 2. 启动时初始化 MCP 连接池\n@Component\npublic class AgentBootrap {\n @PostConstruct\n public void start() {\n // 初始化大模型 maas 服务\n HttpRequestProxy.startHttpPools(\"maas.properties\");\n // 初始化 mcp 服务\n HttpRequestProxy.startHttpPools(\"mcpserver.properties\");\n }\n}\n\n// 3. Spring Boot 配置类注册 MCP 工具\n@Configuration\npublic class EcopAgentMcpClientFactory {\n @Bean(\"ecopConfigMcpServerToolsRegist\")\n public ToolsRegist buildEcopConfigMcpServerToolsRegist() {\n // MCPToolsRegist 通过服务名称关联到 mcpserver.properties 配置\n ToolsRegist toolsRegist = new MCPToolsRegist(\"mcp_server\");\n return toolsRegist;\n }\n}\n\n// 4. 在 AIAgent 中使用 MCP 工具\n@Service\npublic class RagQAService {\n @Autowired\n @Qualifier(\"ecopConfigMcpServerToolsRegist\")\n private ToolsRegist mcpToolsRegist;\n\n public ServerEvent chat(String question) {\n AIAgent agent = new AIAgent();\n agent.setToolsRegist(mcpToolsRegist);\n\n ChatAgentMessage message = new ChatAgentMessage();\n message.setPrompt(question);\n message.setModel(\"deepseek-chat\");\n\n return agent.chat(\"maasName\", message);\n }\n}\n```\n\n#### 9.4.4**配置说明**\n\n| 配置项 | 说明 |\n| -------------------------------------------------- | ---------------------------- |\n| `http.poolNames` | 连接池名称列表,对应服务标识 |\n| `{poolName}.http.hosts` | MCP 服务端地址 |\n| `{poolName}.http.apiKeyId` | 访问 MCP 服务端的 API 密钥 |\n| `{poolName}.http.extendConfigs.streamableendpoint` | Streamable HTTP 端点路径 |\n| `{poolName}.http.extendConfigs.sseendpoint` | SSE 端点路径(可选) |\n\n#### 9.4.5**MCPToolsRegist 工作原理**\n\n1. `MCPToolsRegist` 通过服务名称(如 `mcp_server`)关联到 `mcpserver.properties` 中的连接池配置\n\n2. 初始化时向 MCP 服务端发送 `initialize` 请求进行协议握手\n\n3. 调用 `tools/list` 获取服务端暴露的工具列表\n\n4. 将工具列表转换为 `FunctionToolDefine` 供 `AIAgent` 使用\n\n5. 当 AI 模型需要调用工具时,`MCPToolsRegist` 通过 `tools/call` 将请求转发到 MCP 服务端执行\n\n#### 9.4.6 Spring ai mcp服务兼容性设置\n\n采用streamable api协议对接Spring ai mcp服务时,需要额外设置一下http请求拦截器。\n\n编写一个简单的httprequest拦截器SpringAIMcpRequestIntercepter(内置在bboss框架包,无需自行编写,只需进行下一步的配置即可):\n\n```java\npackage org.frameworkset.spi.ai.mcp.intercepter;\n\nimport org.apache.hc.core5.http.EntityDetails;\nimport org.apache.hc.core5.http.HttpException;\nimport org.apache.hc.core5.http.HttpRequest;\nimport org.apache.hc.core5.http.protocol.HttpContext;\nimport org.frameworkset.spi.remote.http.callback.ClientConfigurationHttpRequestInterceptor;\n\nimport java.io.IOException;\n\n/**\n * spring ai mcp 服务兼容mcp请求拦截器\n * @author biaoping.yin\n * @Date 2026/7/31\n */\npublic class SpringAIMcpRequestIntercepter extends ClientConfigurationHttpRequestInterceptor {\n \n @Override\n public void process(HttpRequest httpRequest, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException {\n try {\n \n httpRequest.addHeader(\"Accept\", \"application/json, text/event-stream\");\n } catch (Exception e) {\n throw new RuntimeException(e);\n }\n \n }\n \n}\n```\n\n spring ai mcp 服务兼容mcp请求拦截器,在mcpserver配置文件进行如下配置即可:test为mcp数据源名称\n\n```properties\ntest.http.httpRequestInterceptors=org.frameworkset.spi.ai.mcp.intercepter.SpringAIMcpRequestIntercepter\n```\n\n\n\n### 9.5 MCP 服务端\n\nbboss-ai 不仅可以作为 MCP 客户端调用外部工具,还可以作为 MCP 服务端对外暴露工具能力:\n\n#### **9.5.1 核心组件**\n\n| 类名 | 作用 |\n| ------------------------------------------------ | ------------------------------------------------------------ |\n| `MCPToolService` / `MCPToolServiceImpl` | MCP 服务端接口实现,提供 SSE 和 Streamable HTTP 两种服务端接口 |\n| `MCPApiKeyService` / `MCPApiKeyServiceImpl` | API 密钥认证与工具注册管理 |\n| `MCPBeanToolsRegist` / `MCPBeanToolFunctionCall` | Bean 工具服务端注册与调用 |\n| `MCPApiRequestUtil` | 服务端请求响应构建工具 |\n\n#### 9.5.2 **服务端能力**\n\n- **SSE 模式**:`sse()` 建立 SSE 长连接,`message()` 处理客户端消息\n- **Streamable HTTP 模式**:`streamable()` 处理无状态请求\n- **API 密钥认证**:支持按 apiKey 注册和授权访问工具\n- **工具权限控制**:支持按 functionName + apiKey 细粒度授权\n- **支持的方法**:initialize、notifications/initialized、tools/list、tools/call\n\n#### 9.5.3 **服务端工作流程**\n\n1. 客户端通过 `registMcpBeanTool(apiKey, bean)` 注册 Bean 工具到指定密钥\n2. 客户端通过 SSE 端点建立连接或发送 Streamable HTTP 请求\n3. 服务端校验 apiKey 合法性\n4. 根据请求方法返回工具列表或执行工具调用\n5. 通过 SSE Sink 或 HTTP Response 返回结果\n\n#### 9.5.4 **Spring Boot 集成示例**\n\n```java\n// 1. 使用 @Tool 注解定义工具\n@Service\npublic class Hotel2ndFlightBookTool {\n @Tool(name=\"hotelQuery\", description=\"根据用户的行程需求,查询合适的酒店。\")\n public List<Map> hotelQuery(\n @ToolParam(name=\"startDay\", description=\"入驻时间,例如:5月25日\", required=true) String startDay,\n @ToolParam(name=\"endDay\", description=\"离房时间,例如:5月28日\", required=true) String endDay\n ) {\n // 业务逻辑实现\n return hotels;\n }\n\n @Tool(name=\"flightQuery\", description=\"根据用户的行程需求,查询合适的航班机票。\")\n public List<Map> flightQuery(\n @ToolParam(name=\"bookDay\", description=\"出发时间,例如:5月25日\", required=true) String bookDay,\n @ToolParam(name=\"arriveDay\", description=\"到达时间,例如:5月28日\", required=true) String arriveDay,\n @ToolParam(name=\"fromStation\", description=\"出发地,例如:长沙\", required=true) String fromStation,\n @ToolParam(name=\"toStation\", description=\"到达地,例如:北京\", required=true) String toStation\n ) {\n // 业务逻辑实现\n return flights;\n }\n}\n\n// 2. Spring Boot 配置类注册 MCP 服务\n@Configuration\npublic class EcopAgentMcpServiceFactory {\n @Autowired\n private Hotel2ndFlightBookTool hotel2ndFlightBookTool;\n\n @Bean(\"ecopConfigMCPToolService\")\n public MCPToolService buildEcopConfigMCPToolService() {\n MCPApiKeyServiceImpl mcpApiKeyService = new MCPApiKeyServiceImpl();\n // 将组件中定义的工具方法注册为 MCP 服务\n mcpApiKeyService.registMcpBeanTool(\"123456\", hotel2ndFlightBookTool);\n\n MCPToolServiceImpl mcpService = new MCPToolServiceImpl();\n mcpService.setMcpApiKeyService(mcpApiKeyService);\n return mcpService;\n }\n}\n\n// 3. Spring Boot Controller 暴露 MCP 端点\n@RestController\n@RequestMapping(\"/mcp\")\npublic class MCPServerController {\n @Autowired\n @Qualifier(\"ecopConfigMCPToolService\")\n private MCPToolService ecopConfigMCPToolService;\n\n // SSE 模式端点\n @GetMapping(\"/sse\")\n public Flux<String> sse(@RequestHeader(name=\"Authorization\") String authorizationHeader) {\n String apiKey = HttpRequestProxy.extractApiKeyFromBearer(authorizationHeader);\n return ecopConfigMCPToolService.sse(apiKey);\n }\n\n // SSE 消息处理端点\n @PostMapping(\"/message\")\n public String message(\n @RequestHeader(name=\"Authorization\") String authorizationHeader,\n @RequestParam(name = \"sessionId\") String sessionId,\n @RequestBody String requestBody\n ) {\n String apiKey = HttpRequestProxy.extractApiKeyFromBearer(authorizationHeader);\n return ecopConfigMCPToolService.message(apiKey, sessionId, requestBody);\n }\n\n // Streamable HTTP 模式端点\n @PostMapping(\"/streamable\")\n public Object streamable(\n @RequestHeader(name=\"Authorization\") String authorizationHeader,\n @RequestBody String requestBody\n ) {\n String apiKey = HttpRequestProxy.extractApiKeyFromBearer(authorizationHeader);\n return ecopConfigMCPToolService.streamable(apiKey, requestBody);\n }\n}\n```\n\n\n\n## 十、视频识别与生成\n\n### 10.1 视频内容识别\n\n```java\npublic static void videoRecognition() {\n VideoVLAgentMessage videoMsg = new VideoVLAgentMessage();\n videoMsg.setModel(\"kimi-k2.5\");\n videoMsg.setPrompt(\"识别视频内容\");\n\n // 使用 Base64 编码的视频文件\n String base64 = FileUtil.getBase64Video(\"/path/to/video.mp4\");\n videoMsg.addVideoUrl(base64);\n\n // 可选参数\n videoMsg.addMapParameter(\"thinking\", \"type\", \"disabled\");\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.videoParser(\"kimi\", videoMsg);\n System.out.println(response.getData());\n}\n```\n\n### 10.2 提交视频生成任务\n\n```\npublic static void submitVideoTask() {\n VideoAgentMessage videoMsg = new VideoAgentMessage();\n videoMsg.setModel(\"wan2.6-t2v\");\n videoMsg.setPrompt(\"一只可爱的小猫在花园里玩耍\");\n \n // 可选参数\n videoMsg.addParameter(\"size\", \"1280*720\");\n videoMsg.addParameter(\"duration\", 10);\n videoMsg.addParameter(\"prompt_extend\", true);\n videoMsg.addParameter(\"watermark\", true);\n \n AIAgent aiAgent = new AIAgent();\n VideoTask task = aiAgent.submitVideoTask(\"qwenvlplus\", videoMsg);\n \n System.out.println(\"任务 ID: \" + task.getTaskId());\n System.out.println(\"任务状态: \" + task.getTaskStatus());\n}\n```\n\n### 10.3 查询视频生成结果\n\n```\npublic static void getVideoTaskResult(String taskId) {\n VideoStoreAgentMessage queryMsg = new VideoStoreAgentMessage();\n queryMsg.setTaskId(taskId);\n queryMsg.setStoreFilePathFunction(url -> {\n return \"video/\" + System.currentTimeMillis() + \".mp4\";\n });\n \n AIAgent aiAgent = new AIAgent();\n VideoGenResult result = aiAgent.getVideoTaskResult(\"qwenvlplus\", queryMsg);\n \n System.out.println(\"任务状态: \" + result.getTaskStatus());\n System.out.println(\"视频 URL: \" + result.getVideoGenUrl());\n System.out.println(\"本地保存路径: \" + result.getVideoUrl());\n}\n```\n\n---\n## 十一、私有Maas平台对接\n\nbboss默认做了对国内主流公有云maas平台的兼容和支持,包括DeepSeek、Kimi、智谱、阿里百炼通义千问、字节豆包火山引擎、MiniMax、腾讯混元、中国移动九天等主流 MaaS 平台。\n\n如果需要对接遵循openai规范的本地私有Maas平台,只需编写自定义的适配器,并进行注册即可,本节介绍私有Maas平台对接方法。\n\n### 11.1 编写自定义适配器\n\n首先,编写自定义适配器,示例如下:直接继承已有通义千问适配器QwenAgentAdapter或者其他现有适配器,重载各种多模态能力相对地址获取方法即可\n\n```java\npackage org.frameworkset.spi.ai;\n\nimport org.frameworkset.spi.ai.adapter.QwenAgentAdapter;\nimport org.frameworkset.spi.ai.model.*;\n\n\npublic class CustomAgentAdapter extends QwenAgentAdapter {\n //LLM文本对话地址\n @Override\n public String getChatCompletionsUrl(ChatAgentMessage chatAgentMessage) {\n return \"/compatible-mode/v1/chat/completions\";\n }\n //提交生成视频任务地址\n @Override\n public String getSubmitVideoTaskUrl(VideoAgentMessage videoAgentMessage){\n if(videoAgentMessage.getFirstFrameUrl() != null) {\n return \"/api/v1/services/aigc/image2video/video-synthesis\";\n }\n else {\n return \"/api/v1/services/aigc/video-generation/video-synthesis\";\n }\n }\n //语音生成地址\n @Override\n public String getGenAudioCompletionsUrl(AudioAgentMessage audioAgentMessage){\n return \"/api/v1/services/aigc/multimodal-generation/generation\";\n }\n\n /**\n * maas平台音频识别服务地址\n * @param audioSTTAgentMessage\n * @return\n */\n @Override\n\n public String getAudioSTTCompletionsUrl(AudioSTTAgentMessage audioSTTAgentMessage){\n return \"/api/v1/services/aigc/multimodal-generation/generation\";\n }\n //视频识别地址\n @Override\n public String getVideoVLCompletionsUrl(VideoVLAgentMessage videoVLAgentMessage) {\n return \"/v1/chat/completions\";\n }\n //图像识别地址\n @Override\n public String getImageVLCompletionsUrl(ImageVLAgentMessage imageVLAgentMessage) {\n return \"/compatible-mode/v1/chat/completions\";\n }\n //图像生成地址\n @Override\n public String getGenImageCompletionsUrl(ImageAgentMessage imageAgentMessage) {\n return \"/api/v1/services/aigc/multimodal-generation/generation\";\n }\n //查询和获取视频任务执行结果地址,执行任务后返回结果中,包含生成的视频url(用于下载),已下载视频本地访问地址\n @Override\n public String getVideoTaskResultUrl(VideoStoreAgentMessage videoStoreAgentMessage){\n return \"/api/v1/tasks/\"+videoStoreAgentMessage.getTaskId();\n }\n}\n\n```\n\n如果有涉及到相关模型协议规范的适配调整,可以重载父类的其他方法。\n\n### 11.2 注册适配器\n\n可以通过以下两种方法注册自定义适配器:\n\n#### 方法1 配置文件方式(推荐)\n\n```properties\nhttp.poolNames = deepseek,guiji,qwenvlplus,volcengine,jiutian,kimi,zhipu,minimax,hunyuan,custom\n\ncustom.http.maxTotal = 200\ncustom.http.defaultMaxPerRoute = 200\n# 配置maas平台地址\ncustom.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\n\ncustom.http.apiKeyId = sk-469f01d6d6db4fb2f4\n# 模型类型:AI智能体工具通过模型类型查找智能体模型对接适配器\ncustom.http.modelType = custom\n# 模型对接适配器配置\ncustom.http.agentAdapter=org.frameworkset.spi.ai.CustomAgentAdapter\n\n```\n\n说明:\n\n1. 定义一个名称为custom(名称可以根据实际情况定)的maas服务,并添加到http.poolNames中\n\n ```properties\n http.poolNames = deepseek,guiji,qwenvlplus,volcengine,jiutian,kimi,zhipu,minimax,hunyuan,custom\n ```\n\n\n\n2. 配置maas平台地址和apiKey\n\n ```properties\n custom.http.hosts=https://dashscope.aliyuncs.com\n ```\n\ncustom.http.apiKeyId = sk-469f01dbb5724e6dbb2f4\n ```\n\n\n\n3. 指定模型平台类型(可选)\n\n ```properties\n custom.http.modelType = custom\n ```\n\n 可以不指定,这时会使用hosts(maas平台地址)作为模型类型\n\n4. 模型对接适配器配置,配置定义好的自定义适配器类路径\n\n ```properties\n custom.http.agentAdapter=org.frameworkset.spi.ai.CustomAgentAdapter\n ```\n\n#### 方法2 代码API配置(不推荐)\n\n首先,还是要在配置文件中配置maas平台服务:只是不需要指定模型平台适配器\n\n```properties\nhttp.poolNames = deepseek,guiji,qwenvlplus,volcengine,jiutian,kimi,zhipu,minimax,hunyuan,custom\n\ncustom.http.maxTotal = 200\ncustom.http.defaultMaxPerRoute = 200\n# 配置maas平台地址\ncustom.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\n\ncustom.http.apiKeyId = sk-469f01d6d6db4fb2f4\n# 模型类型:AI智能体工具通过模型类型查找智能体模型对接适配器\ncustom.http.modelType = custom\n```\n\n然后在加载maas平台服务时,注册模型适配器:\n\n```java\nAgentAdapterFactory.registerAgentAdapter(\"custom\",CustomAgentAdapter.class);//注册模型平台适配器\nHttpRequestProxy.startHttpPools(\"application-stream.properties\");\n```\n## 十二、背压与缓冲控制\n\n```java\nFlux<ServerEvent> flux = aiAgent.streamChat(\"deepseek\", chatMsg)\n .limitRate(5) // 限制请求速率,每次最多 5 个元素\n .buffer(3); // 每 3 个元素缓冲一次\n\nflux.subscribe(bufferedEvents -> {\n for (ServerEvent event : bufferedEvents) {\n // 批量处理事件\n }\n});\n```\n\n---\n\n## 十三、完整示例:多轮智能问答助手\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.*;\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\nimport reactor.core.publisher.Flux;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CountDownLatch;\n\npublic class IntelligentAssistant {\n private static List<Map<String, Object>> sessionMemory = new ArrayList<>();\n private static AIAgent aiAgent = new AIAgent();\n \n static {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n }\n \n public static void chat(String message, boolean reset) throws InterruptedException {\n if (reset) {\n sessionMemory.clear();\n System.out.println(\"会话已重置\");\n }\n \n ChatAgentMessage chatMsg = new ChatAgentMessage();\n chatMsg.setModel(\"deepseek-chat\");\n chatMsg.setPrompt(message);\n chatMsg.setStream(true);\n chatMsg.setSessionMemory(sessionMemory);\n chatMsg.setSessionSize(50);\n chatMsg.setMaxTokens(8192);\n chatMsg.setTemperature(0.7);\n \n CountDownLatch latch = new CountDownLatch(1);\n StringBuilder answer = new StringBuilder();\n \n aiAgent.streamChat(\"deepseek\", chatMsg)\n .doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n answer.append(chunk.getData());\n }\n })\n .doOnComplete(() -> {\n // 保存回答到会话记忆\n if (answer.length() > 0) {\n chatMsg.addSessionMessage(answer.toString());\n }\n System.out.println();\n latch.countDown();\n })\n .doOnError(error -> {\n System.err.println(\"错误: \" + error.getMessage());\n latch.countDown();\n })\n .subscribe();\n \n latch.await();\n }\n \n public static void main(String[] args) throws InterruptedException {\n chat(\"你好,我叫小明\", false);\n chat(\"我叫什么名字?\", false);\n chat(\"重置会话\", true);\n chat(\"我叫什么名字?\", false);\n }\n}\n```\n\n---\n\n## 十四、多智能体编排工作流\n\nbboss-ai-flow 模块提供了一套强大的智能体工作流编排能力,基于有向循环图实现多智能体协同。支持串行、并行、路由、条件分支、裁判评估等丰富的流程编排模式,能够快速构建复杂的多智能体系统。\n\n\n\n多智能体编排工作流底层基于[bboss jobflow](https://esdoc.bbossgroups.com/#/jobworkflow)实现(一套数据交换作业编排工作流)\n\n### 14.1 工作流核心组件\n\n#### 14.1.1工作流节点类型\n\n| 组件 | 说明 |\n|------|------|\n| `AIPlanAgent` | 工作流编排主控制器,负责构建和执行整个智能体工作流 |\n| `AINodeAgent` | 普通AI节点智能体,执行具体的AI任务 |\n| `UserNodeAgent` | 用户智能体,不引用上游父智能体的会话记忆,适合处理独立任务 |\n| `AIParrelAgent` | 并行智能体容器,内部多个子智能体并发执行 |\n| `AISequenceAgent` | 串行智能体容器,内部多个子智能体按顺序执行 |\n| `AIRouteAgent` | 基于LLM路由智能体,根据用户问题自动决策后续执行路径 |\n| AIKeywordsRouteAgent | 基于关键词数组路由智能体,根据用户问题和关键词自动决策后续执行路径 |\n| `AIJudgeAgent` | 裁判智能体,评估执行结果是否满足预期,内置节点及变量:#[input.query,scope=node]<br/>#[answer,scope=node] |\n| `StoreContext` | 会话存储上下文,支持内存和数据库两种持久化方式 |\n| StandaloneAgent | 独立智能体,不会接受上游消息,也不会向下游推送消息 |\n| `AIFlowNode` | 通用工作流节点,执行纯Java自定义逻辑,不调用大模型,可在工作流中加工和传递数据、记录流程过程数据<br/>,接口方法call带返回值 |\n| AIFlowNodeVoid | 通用工作流节点,执行纯Java自定义逻辑,不调用大模型,可在工作流中加工和传递数据、记录流程过程数据<br/>,接口方法call不需要返回值 |\n\n| 节点类型 | 继承类 | 特点 |\n| ------------ | --------------- | ------------------------------------------------------------ |\n| AI节点 | `AINodeAgent` | 标准AI智能体,自动引用父智能体会话记忆 |\n| 用户节点 | `UserNodeAgent` | 不引用上游父智能体记忆,适合独立任务或工具调用 |\n| 路由节点 | `AIRouteAgent` | 负责路由决策,不直接回答问题 |\n| 裁判节点 | `AIJudgeAgent` | 评估结果质量,输出判断结论 |\n| 通用流程节点 | `AIFlowNode` | 执行纯Java自定义逻辑,不调用大模型,可在工作流中加工和传递数据、记录流程过程数据 |\n\n\n\n#### 14.1.2 会话存储配置\n\n工作流支持将会话记录持久化到数据库、Clickhouse或内存中:\n\n##### 关系数据库会话存储设置\n\n```java\nimport org.frameworkset.spi.ai.store.StoreContext;\n\n// 关系数据库存储方式\nStoreContext storeContext = new StoreContext()\n .setSessionId(\"session_001\") // 会话ID\n .setUserId(\"user123\") // 用户ID\n .setRequestId(\"request123\") // 请求ID\n .setSessionSize(100) // 会话窗口大小\n .setStoreType(StoreContext.STORE_TYPE_DB) // 数据库存储\n .setDataSource(\"visualops\"); // 数据源名称\n\n\n```\n\n关系数据库数据源声明:\n\n```java\nSQLUtil.startPool(\"visualops\",//数据源名称\n \"com.mysql.cj.jdbc.Driver\",//oracle驱动\n \"jdbc:mysql://192.168.137.1:3306/bboss?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true\",//mysql链接串\n \"root\",\"123456\",//数据库账号和口令\n \"select 1 \" //数据库连接校验sql\n);\n```\n\n##### Clickhouse会话存储配置\n\n<a id=\"chatper14-122\"></a>\n\nClickhouse存储会话配置,生产环境推荐使用Clickhouse存储\n\n```java\nStoreContext storeContext = new StoreContext()\n.setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100) \n.setStoreType(StoreContext.STORE_TYPE_DB)\n .setRequestId(\"request123\")\n .setClickhouseCluster(\"vops_3shards_1replicas\")\n.setDataSource(\"visualops\")\n```\n\n\n\nClickhouse数据源声明\n\n```java\nDBConf tempConf = new DBConf();\ntempConf.setPoolname(\"visualops\");\ntempConf.setDriver(\"com.clickhouse.jdbc.ClickHouseDriver\");\ntempConf.setJdbcurl(\"jdbc:clickhouse:http://100.13.6.4:28123,100.13.6.7:28123,100.13.4.6:28123/visualops?b.enableBalance=true&b.balance=roundbin\");\ntempConf.setUsername(\"default\");\ntempConf.setPassword(\"123456\");\ntempConf.setValidationQuery(\"select 1 \");\n//tempConf.setTxIsolationLevel(\"READ_COMMITTED\");\ntempConf.setJndiName(\"jndi-visualops\" );\ntempConf.setInitialConnections(10);\ntempConf.setMinimumSize(10);\ntempConf.setMaximumSize(20);\ntempConf.setUsepool(true);\n\ntempConf.setShowsql(true);\nSQLManager. startPool(tempConf);\n```\n\nClickhouse数据源注意事项\n\n1)clickhouse 会话存储支持:通过框架自动创建表时,需要指定 clickhouse 集群名称,为每个集群节点定义名称为shard和replica的两个宏变量。\n\n通过以下方式设置clickhouse表集群名称。\n\n```java\nstoreContext.setClickhouseCluster(\"vops_3shards_1replicas\")\n```\n\nshard和replica两个宏变量配置,参考Clickhouse官方文档。\n\n如果通过框架自动建表,clickhouse 表集群名称一定要设置,否则报错,如果表已经提起创建创建好了,可以不用创建\n\n2)使用Clickhouse时,会话续问续答时,不会更新会话修改时间\n\n自行创建Clickhouse会话表时,参考文档:\n\nhttps://gitee.com/bboss/bboss-ai/blob/main/bboss-ai/src/main/java/org/frameworkset/spi/ai/store/db/clickhouse-agent.xml\n\n##### 内存会话存储配置\n\n```java\n// 内存存储方式(默认)\nStoreContext memoryContext = new StoreContext()\n .setSessionSize(50)\n .setStoreType(StoreContext.STORE_TYPE_MEMORY);\n```\n\n\n\n#### 14.1.3 工作流基本结构\n\n```java\nimport org.frameworkset.spi.ai.flow.*;\nimport org.frameworkset.spi.ai.model.ChatAgentMessage;\nimport org.frameworkset.spi.ai.model.LastSessionMessage;\nimport org.frameworkset.spi.ai.store.StoreContext;\n\n// 1. 定义会话消息\nChatAgentMessage chatAgentMessage = new ChatAgentMessage()\n .setModel(\"qwen3.6-plus\")\n .setMaas(\"qwenvlplus\")\n .setPrompt(\"介绍省份智能体\");\n\n// 2. 定义工作流智能体\nAIPlanAgent aiPlanAgent = new AIPlanAgent(storeContext)\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\")\n .setAgentId(\"workflowAgent\");\n\n// 3. 添加工作流节点...\n\n// 4. 执行工作流\nLastSessionMessage result = aiPlanAgent.chat();\n```\n\n\n\n### 14.2 串行任务编排\n\n串行智能体(`AISequenceAgent`)将多个子智能体按顺序执行,前一个智能体的输出会作为后续智能体的上下文输入。\n\n#### 14.2.1 同步串行工作流\n\n```java\n// 构建工作流智能体\nAIPlanAgent aiPlanAgent = new AIPlanAgent(new StoreContext()\n .setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100)\n .setStoreType(StoreContext.STORE_TYPE_DB).setRequestId(\"request123\")\n .setDataSource(\"visualops\"))\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\");\n\n// 添加第一个普通节点\nAIBaseNodeAgent introduceProvinces = new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setAgentName(\"介绍中国省份和直辖市\")\n .setAgentId(\"introduceProvinces\");\naiPlanAgent.addAgent(introduceProvinces);\n\n// 构建串行智能体\nAISequenceAgent sequenceAgent = new AISequenceAgent(aiPlanAgent)\n .setAgentId(\"sequenceAgent\")\n .setAgentName(\"串行任务节点\");\n\n// 按顺序添加子智能体\nsequenceAgent.addAgent(new AINodeAgent(\"用50字介绍湖南,并且和介绍中国省份和直辖市内容合并输出\")\n .setAgentId(\"jieshaohunan\")\n .setAgentName(\"用50字介绍湖南\"));\nsequenceAgent.addAgent(new AINodeAgent(\"用50字介绍湖北\")\n .setAgentId(\"jieshaohubei\")\n .setAgentName(\"用50字介绍湖北\"));\nsequenceAgent.addAgent(new AINodeAgent(\"用50字介绍江西\")\n .setAgentId(\"jieshaojiangxi\")\n .setAgentName(\"用50字介绍江西\"));\nsequenceAgent.addAgent(new AINodeAgent(\"将下面的文字翻译为英文(不要回答问题):用50字介绍江西\")\n .setAgentId(\"translate\")\n .setAgentName(\"将文字翻译为英文\"));\n\n// 将串行智能体加入主工作流\naiPlanAgent.addAgent(sequenceAgent);\n\n// 执行工作流\nLastSessionMessage lastSessionMessage = aiPlanAgent.chat();\nif (lastSessionMessage != null) {\n logger.info(\"结果: {}\", lastSessionMessage.getData());\n}\n```\n\n#### 14.2.2 流式串行工作流\n\n流式模式与同步模式结构一致,只需将 `chat()` 替换为 `chatStream()`:\n\n```java\nFlux<ServerEvent> flux = aiPlanAgent.chatStream();\n\nStringBuilder completeAnswer = new StringBuilder();\nCountDownLatch countDownLatch = new CountDownLatch(1);\n\nflux.doOnNext(event -> {\n if (event.getData() != null) {\n System.out.print(event.getData());\n }\n if (!event.isDone() && event.getData() != null) {\n completeAnswer.append(event.getData());\n } else {\n if (completeAnswer.length() > 0) {\n chatAgentMessage.addAgentResultSessionMessage(\n completeAnswer.toString(), event.getAgent());\n completeAnswer.setLength(0);\n }\n }\n})\n.doOnComplete(() -> {\n logger.info(\"\\n=== 流完成 ===\");\n countDownLatch.countDown();\n})\n.doOnError(error -> {\n logger.error(\"错误: \" + error.getMessage(), error);\n countDownLatch.countDown();\n})\n.subscribe();\n\ncountDownLatch.await();\n```\n\n#### 14.2.3 条件分支节点\n\n可以为节点添加条件触发器,控制节点是否执行:\n\n```java\nimport org.frameworkset.tran.jobflow.script.TriggerScriptAPI;\nimport org.frameworkset.tran.jobflow.context.NodeTriggerContext;\n\nIntegerCount integerCount = new IntegerCount();\n\n// 为 introduceProvinces 节点添加条件:仅第一次执行时触发\naiPlanAgent.addConditionFlowNode(introduceProvinces, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int i = integerCount.increament();\n return i == 1;\n }\n});\n```\n\n### 14.3 并行任务编排\n\n并行智能体(`AIParrelAgent`)将多个子智能体并发执行,所有子任务完成后自动聚合结果。\n\n#### 14.3.1 同步并行工作流\n\n```java\n// 构建工作流智能体\nAIPlanAgent aiPlanAgent = new AIPlanAgent(storeContext)\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\");\n\n// 添加初始节点\naiPlanAgent.addAgent(new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setAgentName(\"介绍中国省份和直辖市\")\n .setAgentId(\"introduceProvinces\"));\n\n// 构建并行智能体\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent)\n .setAgentId(\"aiParrelAgent\")\n .setAgentName(\"共享任务节点\");\n\n// 添加多个并行子智能体\naiParrelAgent.addAgent(new AINodeAgent(\"用50字介绍湖南\")\n .setAgentId(\"jieshaohunan\")\n .setAgentName(\"用50字介绍湖南\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍湖北\")\n .setAgentId(\"jieshaohubei\")\n .setAgentName(\"用50字介绍湖北\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍江西\")\n .setAgentId(\"jieshaojiangxi\")\n .setAgentName(\"用50字介绍江西\"));\n\n// 将并行智能体加入主工作流\naiPlanAgent.addAgent(aiParrelAgent);\n\n// 执行\nLastSessionMessage result = aiPlanAgent.chat();\n```\n\n#### 14.3.2 流式并行工作流\n\n```java\nFlux<ServerEvent> flux = aiPlanAgent.chatStream();\n\nStringBuilder completeAnswer = new StringBuilder();\nCountDownLatch countDownLatch = new CountDownLatch(1);\n\nflux.doOnSubscribe(subscription -> logger.info(\"开始订阅流...\"))\n .doOnNext(event -> {\n // 首条消息可添加扩展链接\n if (event.isFirst() && !event.isToolCallResponse()) {\n event.addExtendData(\"url\", \"https://www.bbossgroups.com\");\n event.addExtendData(\"title\", \"bboss官网\");\n }\n\n if (event.isDone()) {\n event.addExtendData(\"url\", \"https://www.bbossgroups.com\");\n event.addExtendData(\"title\", \"bboss官网\");\n }\n\n if (event.getData() != null) {\n System.out.print(event.getData());\n }\n\n if (event.isToolCallsType()) {\n System.out.println(\"\\n开始执行工具:\");\n }\n\n if (event.isDone() || event.finished()) {\n System.out.println();\n }\n\n if (!event.isDone()) {\n if (event.getData() != null) {\n completeAnswer.append(event.getData());\n }\n } else {\n if (completeAnswer.length() > 0) {\n chatAgentMessage.addAgentResultSessionMessage(\n completeAnswer.toString(), event.getAgent());\n completeAnswer.setLength(0);\n }\n }\n })\n .doOnComplete(() -> {\n logger.info(\"\\n=== 流完成 ===\");\n countDownLatch.countDown();\n })\n .doOnError(error -> {\n logger.error(\"错误: \" + error.getMessage(), error);\n countDownLatch.countDown();\n })\n .subscribe();\n\ncountDownLatch.await();\n```\n\n#### 14.3.3 并行结果聚合\n\n并行智能体执行完毕后,会自动调用 `buildResult()` 方法聚合所有子智能体的结果。默认格式为:\n\n```java\nagentId1:子智能体1的结果\nagentId2:子智能体2的结果\n```\n\n如需自定义聚合格式,可重载 `buildResult()` 方法:\n\n```java\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent) {\n @Override\n public String buildResult(List<LastSessionMessage> lastSessionMessages) {\n StringBuilder builder = new StringBuilder();\n for (LastSessionMessage msg : lastSessionMessages) {\n if (builder.length() > 0) {\n builder.append(\"\\n---\\n\");\n }\n builder.append(\"[\").append(msg.getMsgAgentId()).append(\"]\\n\")\n .append(msg.getData());\n }\n return builder.toString();\n }\n};\n```\n\n### 14.4 条件任务节点和触发器\n\n通过条件任务节点,可以在工作流或者并行/串行分支中,添加多个候选条件任务分支节点以及一个默认分支节点(可行),每个条件任务分支节点需指定特定的条件触发器。\n\n流程调度执行时,会从所以条件分支任务节点选择满足触发条件的任务分支节点执行,如果没有满足的节点,则会选择默认分支节点执行;如果既没有满足条件的条件分支节点,也没有设置默认分支节点,则会根据控制参数allCondtionNodeMatchedfailedContinue的值,来判别是否继续执行条件节点后面的任务节点(如果有),true,则执行,false不执行,直接终止当前条件节点所在流程或者并行/串行分支的执行。\n\n基于条件任务节点,可以实现:\n\n1. if-elseif-else条件决策树编排\n2. 有限循环图智能体流程编排\n3. 路由智能体编排\n\n#### 14.4.1 简单条件任务分支\n\n如果选择的智能体id为docAgent,则执行飞书文档智能体\n\n如果选择的智能体id为logAgent,则执行日志分析智能体\n\n如果没有选择具体的智能体,则执行默认智能体,直接回答用户问题\n\n```java\n//添加一个简单的决策智能体节点,在流程上下文中添加路由的agentId\nplanAgent.addAgent(new AIFlowNode() {\n /**\n * 由子类继承和实现\n *\n * @param jobFlowNodeExecuteContext\n * @return\n */\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n jobFlowNodeExecuteContext.addJobFlowContextData(\"routeChoice\",\"docAgent\");//模拟设置后续节点id\n return null;\n }\n });\n ToolsRegist feishuMcp = new FeishuMcpRegist(\"feishumcp\");\n planAgent.addConditionFlowNode(new UserNodeAgent(feishuMcp).setAgentId(\"docAgent\").setAgentName(\"飞书文档智能体\"),\n nodeTriggerContext -> {\n String agentId = (String) nodeTriggerContext.getFlowContextData(\"routeChoice\",true);\n \n if(agentId != null && agentId.equals(\"docAgent\")){\n return true;\n }\n return false;\n });\n planAgent.addConditionFlowNode(new UserNodeAgent().setAgentId(\"logAgent\").setAgentName(\"日志分析智能体\"),\n nodeTriggerContext -> {\n String agentId = (String) nodeTriggerContext.getFlowContextData(\"routeChoice\",true);\n \n if(agentId != null && agentId.equals(\"logAgent\")){\n return true;\n }\n return false;\n });\n//添加默认条件分支节点\n planAgent.addConditionFlowNode(new UserNodeAgent( ).setAgentId(\"defaultAgent\").setAgentName(\"默认智能体直接回答问题\"),true);\n//开始对话,执行对话流程,并返回会话结果\n Flux<ServerEvent> flux = planAgent.chatStream();\n```\n\n#### 14.4.2 条件分支后开启新条件分支\n\n可以通过以下系列addAnotherConditionJobFlowNodeAgent方法,在当前条件分支后开启一个新的条件分支:\n\n```java\npublic String addAnotherConditionJobFlowNodeAgent(AppendToParentAgent baseNodeAgent, NodeTrigger conditionNodeTrigger)\npublic String addAnotherConditionJobFlowNodeAgent(AppendToParentAgent baseNodeAgent, NodeTrigger conditionNodeTrigger,boolean defaultConditionNode) \npublic String addAnotherConditionJobFlowNodeAgent(boolean allCondtionNodeMatchedfailedContinue,AppendToParentAgent baseNodeAgent, TriggerScriptAPI conditionNodeTrigger)\npublic String addAnotherConditionJobFlowNodeAgent(boolean allCondtionNodeMatchedfailedContinue,AppendToParentAgent baseNodeAgent, TriggerScriptAPI conditionNodeTrigger,boolean defaultConditionNode) \n \n```\n\n注意调用addAnotherConditionJobFlowNodeAgent方法后,表示开启新的条件任务节点,并添加第一个分支,后续条件分支节点则通过addConditionFlowNode方法添加即可。\n\n#### 14.4.3 实现有向循环图\n\n基于条件分支节点,在通过addConditionFlowNode方法和addAnotherConditionJobFlowNodeAgent方法添加条件分支节点时,可以指定为当前流程或者并行、串行分支流程的前序节点(案例中为introduceProvinces)或者后序节点,从而实现有向循环图流程编排功能:\n\n**跳转到前序节点**\n\n```java\nAIPlanAgent planAgent = new AIPlanAgent(new StoreContext()\n .setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100) \n .setStoreType(StoreContext.STORE_TYPE_DB).setRequestId(\"request123\")\n .setDataSource(\"visualops\"))\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\")\n ;\nAIBaseNodeAgent introduceProvinces = new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\").setAgentName(\"介绍中国省份和直辖市\").setAgentId(\"introduceProvinces\");\nplanAgent.addAgent(introduceProvinces);\n//构建并行智能体\nAIParrelAgent aiParrelAgent = new AIParrelAgent(planAgent).setAgentId(\"aiParrelAgent\").setAgentName(\"并行智能体\");\naiParrelAgent.addAgent(new AINodeAgent(\"用50字介绍湖南,并且和介绍中国省份和直辖市内容合并输出\").setAgentId(\"jieshaohunan\").setAgentName(\"用50字介绍湖南\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍湖北\").setAgentId(\"jieshaohubei\").setAgentName(\"用50字介绍湖北\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍江西\").setAgentId(\"jieshaojiangxi\").setAgentName(\"用50字介绍江西\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"将下面的文字翻译为英文(不要回答问题):用50字介绍江西\").setAgentId(\"translate\").setAgentName(\"将文字翻译为英文\"));\nplanAgent.addAgent(aiParrelAgent);\nIntegerCount integerCount = new IntegerCount();\n//直接指向前序节点introduceProvinces,实现有条件循环,多循环一次,就结束循环,第一个参数为true,表示循环结束后继续执行流程后续节点\nplanAgent.addConditionFlowNode(true,introduceProvinces, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int i = integerCount.increament();\n if(i == 1) {\n return true;\n }\n else{\n return false;\n }\n }\n});\n//下面给流程增加了两个通用节点\nplanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点1。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n });\n\n planAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点2。\");\n logger.info(\"call 自定义节点2。customNode:{}\", jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\"));\n return null;\n }\n });\n```\n\n**跳转到后续节点**\n\n```java\n//定义会话实体:设置模型、maas平台,用户问题\nChatAgentMessage chatAgentMessage = new ChatAgentMessage() \n .setModel(model).setThinking(false)\n .setMaas(maas).setPrompt(prompt);\n\n//定义工作流智能体,设置会话存储机制为DB,设置DB数据源、当前会id以及用户id\n// 设置短期会话窗口\nAIPlanAgent planAgent = new AIPlanAgent(new StoreContext()\n .setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100) \n .setStoreType(StoreContext.STORE_TYPE_DB).setRequestId(\"request123\")\n .setDataSource(\"visualops\"))\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\")\n ;\nAIBaseNodeAgent introduceProvinces = new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\").setAgentName(\"介绍中国省份和直辖市\").setAgentId(\"introduceProvinces\");\nplanAgent.addAgent(introduceProvinces);\nplanAgent.addAgent(new AIFlowNode() {\n /**\n * 由子类继承和实现\n *\n * @param jobFlowNodeExecuteContext\n * @return\n */\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n //生成一个10以内的随机整数,如果随机数是偶数则触发节点\n int randomInt = (int) (Math.random() * 10);\n logger.info(\"randomInt:{}\", randomInt);\n jobFlowNodeExecuteContext.addJobFlowContextData(\"randomInt\", randomInt);\n return null;\n }\n});\n//1.定义一个后序条件跳转节点\nAIFlowNode aiFlowNode = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode(满足条件时,会直接跳转到这个节点执行,绕过aiParrelAgent和循环跳转节点introduceProvinces,直接执行aiFlowNode后续流程节点)。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n};\n//2.添加后序条件跳转节点\nplanAgent.addConditionFlowNode(true,aiFlowNode, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int randomInt = (int) nodeTriggerContext.getFlowContextData(\"randomInt\");\n \n if (randomInt % 2 == 0) {\n return true;\n }\n return false;\n }\n});\n\nAIFlowNode aiFlowNode1 = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode1。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData1\");\n return null;\n }\n};\nplanAgent.addConditionFlowNode(aiFlowNode1, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n //生成一个10以内的随机整数,如果随机数是奇数则触发节点\n int randomInt = (int) nodeTriggerContext.getFlowContextData(\"randomInt\");\n if (randomInt % 2 != 0) {\n return true;\n }\n return false;\n }\n});\n\nAIFlowNode aiFlowNode2 = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode2。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData2\");\n return null;\n }\n};\nplanAgent.addConditionFlowNode(aiFlowNode2, true);\n//构建并行子智能体\nAIParrelAgent aiParrelAgent = new AIParrelAgent(planAgent).setAgentId(\"aiParrelAgent\").setAgentName(\"并行智能体\");\naiParrelAgent.addAgent(new AINodeAgent(\"用50字介绍湖南,并且和介绍中国省份和直辖市内容合并输出\").setAgentId(\"jieshaohunan\").setAgentName(\"用50字介绍湖南\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍湖北\").setAgentId(\"jieshaohubei\").setAgentName(\"用50字介绍湖北\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍江西\").setAgentId(\"jieshaojiangxi\").setAgentName(\"用50字介绍江西\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"将下面的文字翻译为英文(不要回答问题):用50字介绍江西\").setAgentId(\"translate\").setAgentName(\"将文字翻译为英文\"));\nplanAgent.addAgent(aiParrelAgent);\nIntegerCount integerCount = new IntegerCount();\nplanAgent.addConditionFlowNode(true,introduceProvinces, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int i = integerCount.increament();\n if(i == 1) {\n return true;\n }\n else{\n return false;\n }\n }\n});\n//3.添加后序条件跳转节点到主流程中,前面的条件分支节点会直接跳转的本节点\nplanAgent.addAgent(aiFlowNode);\nplanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点3。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData3\");\n return null;\n }\n});\n\nplanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点4。customNode:{}\", jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\"));\n return null;\n }\n});\n\n\n//开始对话,执行对话流程,并返回会话结果 \nLastSessionMessage lastSessionMessage = planAgent.chat();\n\n//输出会话结果 \nif(lastSessionMessage != null) {\n String data = lastSessionMessage.getData();\n logger.info(\"serverEvent:{}\", data);\n}\n```\n\n说明:通过条件跳转到流程或者并行分支/串行分支后序节点,可以成功绕过部分中间节点\n\n```java\n//1.定义一个后序条件跳转节点\nAIFlowNode aiFlowNode = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode(满足条件时,会直接跳转到这个节点执行,绕过aiParrelAgent和循环跳转节点introduceProvinces,直接执行aiFlowNode后续流程节点)。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n};\n\n//2.添加后序条件跳转节点\nplanAgent.addConditionFlowNode(true,aiFlowNode, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int randomInt = (int) nodeTriggerContext.getFlowContextData(\"randomInt\");\n \n if (randomInt % 2 == 0) {\n return true;\n }\n return false;\n }\n});\n\n。。。。。。中间节点。。。。。\n \n//3.添加后序条件跳转节点到主流程中,前面的条件分支节点会直接跳转的本节点\nplanAgent.addAgent(aiFlowNode);\n。。。。。。后续节点。。。。。\n```\n\n##### 14.4.3.1 关键说明\n\n通过以下方法添加条件跳转节点,如果条件不成立,会自动终止执行条件节点的后续节点执行:\n\n1)如果条件节点是在工作流也就是终止整个工作流\n\n2)如果条件节点是在串行分支中,则会终止串行分支节点执行\n\n```java\n//2.添加后序条件跳转节点\n planAgent.addConditionFlowNode(AppendToParentAgent aiAgent , TriggerScriptAPI conditionNodeTrigger)\n```\n\n通过以下方法添加条件跳转节点,如果条件不成立,当allCondtionNodeMatchedfailedContinue为true时,会自动执行条件节点的后续节点,不会终止条件节点所在的主干流程或者串行分支流程:\n\n```java\n//2.添加后序条件跳转节点\n planAgent.addConditionFlowNode(boolean allCondtionNodeMatchedfailedContinue,AppendToParentAgent aiAgent , TriggerScriptAPI conditionNodeTrigger)\n```\n\n有限循环图可以基于条件节点来实现的关键点,就在于**allCondtionNodeMatchedfailedContinue**参数,当allCondtionNodeMatchedfailedContinue取值为true时,我们指定条件节点为流程中或者串行分支中之前的节点,如果conditionNodeTrigger触发器判定条件成立,则继续跳转到导之前的节点重复执行,形成循环;如果判定条件不成立,则结束循环,继续执行后续流程节点。\n\n#### 14.4.4 注意事项\n\n- 条件节点只能在当前主干流程节点间跳转,不能跳转到分支复合节点内部\n\n- 条件节点只能在串行分支流程节点之间跳转,不能跳转分支复合节点内部或者分支外部\n\n- 条件节点只能在并行分支中并行分支流程节点之间跳转,不能跳转分支复合节点内部或者分支外部\n\n- 可以为条件节点设置条件触发器,节点可以加入到多个条件分支,每个分支都可以指定特定条件触发器,如果没有指定,则使用节点自身的条件触发器,条件节点必须指定自己的条件触发器\n\n### 14.5 路由智能体\n\n路由智能体(`AIRouteAgent`和AIKeywordsRouteAgent)基于条件任务节点实现,能够根据用户问题自动选择后续执行路径,实现智能分发。典型的**意图识别+路由分发**的AI架构模式,利用 LLM + 关键词规则,自动识别用户意图,然后决定走哪条处理分支,实现意图识别与路由分发机制。\n\n#### 14.5.1 同步路由工作流\n\n```java\nAIPlanAgent aiPlanAgent = new AIPlanAgent(storeContext)\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\");\n\n// 构建路由规则智能体\naiPlanAgent.addAgent(new AIRouteAgent()\n .setAgentId(\"Router\")\n .setAgentName(\"路由规则智能体\")\n .setSystemPrompt(\"你是一个路由智能体。你的目标是将用户查询路由到正确的后续任务,注意你不需要回答用户的问题。\")\n .addRoutingChoice(\"weatherAgent\", \"查询城市天气,并给出穿衣出行建议\")\n .addRoutingChoice(\"docAgent\", \"操作飞书文档\")\n);\n\n// 构建天气查询智能体\naiPlanAgent.addRouteChoiceAgent(new UserNodeAgent(new MCPToolsRegist(\"visualops\"))\n .setAgentId(\"weatherAgent\")\n .setAgentName(\"天气查询智能体\"));\n\n// 构建飞书文档操作智能体\nToolsRegist feishuMcp = new FeishuMcpRegist(\"feishumcp\");\naiPlanAgent.addRouteChoiceAgent(new UserNodeAgent(feishuMcp)\n .setAgentId(\"docAgent\")\n .setAgentName(\"飞书文档智能体\"));\n\n// 构建默认智能体:当用户问题匹配不上时执行\naiPlanAgent.addDefaultRouteChoiceAgent(\n new AINodeAgent().setAgentId(\"defaultAgent\").setAgentName(\"默认智能体\"));\n\n// 构建裁判智能体:判断是否回答了问题\naiPlanAgent.addAgent(new AIJudgeAgent(\"评估结果是否回答了问题,回答请回复:是,否则回复:否\")\n .setAgentId(\"judgeAgent\")\n .setAgentName(\"评估智能体\"));\n\n// 构建条件节点:根据裁判结果决定是否创建飞书文档\naiPlanAgent.addAgent(\n new AINodeAgent(\"将结果创建为飞书文档\", feishuMcp)\n .setAgentId(\"createDocAgent\")\n .setAgentName(\"飞书文档创建智能体\"),\n nodeTriggerContext -> {\n String judgeResult = (String) nodeTriggerContext\n .getFlowContextData(\"judgeAgent.judgeResult\");\n return \"是\".equals(judgeResult);\n });\n\n// 执行\nLastSessionMessage result = aiPlanAgent.chat();\n```\n\n#### 14.5.2 流式路由工作流\n\n流式路由与同步路由结构一致,只需调用 `chatStream()`:\n\n```java\nFlux<ServerEvent> flux = aiPlanAgent.chatStream();\n// ... 流式处理逻辑与并行流式示例相同\n```\n\n#### 14.5.3 关键词路由智能体\n\n```java\n planAgent.addAgent(new AIKeywordsRouteAgent()\n\t\t\t\t.addRoutingChoice(\"bothAgent\", new String[]{\"酒店和机票\"}, \"用户需要同时预定酒店和机票\")\n\t\t\t\t.addRoutingChoice(\"hotelAgent\", new String[]{\"酒店\"},\"用户只需要预定酒店\")\n\t\t\t\t.addRoutingChoice(\"flightAgent\", new String[]{\"航班\",\"机票\"}, \"用户只需要预定机票\")\n\t\t\t\t\n .setAgentId(\"bookingRouter\").setAgentName(\"预定路由智能体\")\n .setSystemPrompt(\"你是一个行程预定路由智能体。请分析用户的问题,判断用户需要预定什么,注意你不需要直接回答用户的问题,只需要做路由判断,使用json格式返回匹配的智能体信息\") \n \n );\n```\n\nAIKeywordsRouteAgent 是轻量级的关键词匹配方案,适合意图明确、关键词边界清晰的场景(如本例中的\"酒店\"、\"机票\")。\n\n### 14.6 通用流程节点\n\nbboss提供了两种类型通用节点:AIFlowNode和AIFlowNodeVoid(二者区别:后者call方法无返回值,视情况选取),通用流程节点用于在工作流中执行纯 Java 代码逻辑,不调用大模型。适合数据转换、参数校验、外部接口调用、数据预处理、记录用户输入和创建用户会话等场景。\n\n记录用户输入信息和创建会话:\n\n```java\n planAgent.addAgent(new AIFlowNodeVoid(){\n \n @Override\n public void call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n // ===== 2. 加载历史会话消息,如果会话不存在,则创建 =====\n planAgent.loadSessionMemory( question, domain);\n TraceMessage traceMessage = new TraceMessage(); \n //记录用户输入的原始问题\n traceMessage.setMessage(Map.of(\"question\", question,\"role\",SessionMessage.MESSAGE_TYPE_USERINPUTMESSAGE_NAME));\n //其他用户上传的附件材料信息可以放到metaData中,也可以直接放到上面的消息中\n// traceMessage.setMetaData(Map.of(\"documents\", new ArrayList<>()));\n traceMessage.setStartTime(System.currentTimeMillis());\n// traceMessage.setEndTime(System.currentTimeMillis());\n planAgent.recordTraceMessage(traceMessage);\n \n \n \n }\n });\n```\n\n其他应用案例:\n\n \n\n```java\nimport org.frameworkset.spi.ai.flow.AIFlowNode;\nimport org.frameworkset.tran.jobflow.context.JobFlowNodeExecuteContext;\n\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点1。\");\n // 向工作流上下文写入数据\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n});\n\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点2。\");\n // 从工作流上下文读取数据\n Object data = jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\");\n logger.info(\"customNode:{}\", data);\n return null;\n }\n});\n\naiPlanAgent.addAgent(new AIFlowNodeVoid() {\n @Override\n public void call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点2。\");\n // 从工作流上下文读取数据\n Object data = jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\");\n logger.info(\"customNode:{}\", data);\n \n});\n```\n\n通用流程节点通过 `JobFlowNodeExecuteContext` 实现与上下游节点的数据交互:\n\n- `addJobFlowContextData(key, value)`:向工作流上下文写入键值对数据\n- `getJobFlowContextData(key)`:从工作流上下文读取数据\n\n### 14.7 评估智能体\n\n```java\n//构建裁判智能体:判断是否回答了问题,可以指定评估提示词,通过变量一样问题和问题答案\naiPlanAgent.addAgent(new AIJudgeAgent(\"评估结果是否回答了问题:\\n#[input.query,scope=node]\\n# 问题答案:\\n#[answer,scope=node],回答请回复:是,否则回复:否\").setAgentId(\"judgeAgent\").setAgentName(\"评估智能体\"));\n\n//构建裁判智能体:如果不指定提示词,则使用默认提示词\n\naiPlanAgent.addAgent(new AIJudgeAgent().setAgentId(\"judgeAgent\").setAgentName(\"评估智能体\"));\n```\n\n\n\n### 14.8 智能体配置参数\n\n### 14.9 工作流组合模式\n\n串行和并行智能体可以相互嵌套,构建复杂的工作流:\n\n```java\n// 串行智能体中嵌入并行智能体\nAISequenceAgent sequenceAgent = new AISequenceAgent(aiPlanAgent);\n\nAIParrelAgent innerParrel = new AIParrelAgent(aiPlanAgent);\ninnerParrel.addAgent(new AINodeAgent(\"任务A\"));\ninnerParrel.addAgent(new AINodeAgent(\"任务B\"));\n\nsequenceAgent.addAgent(innerParrel); // 并行作为串行的一个步骤\nsequenceAgent.addAgent(new AINodeAgent(\"任务C\"));\n\naiPlanAgent.addAgent(sequenceAgent);\n\n// 并行智能体中嵌入串行智能体\nAIParrelAgent parrelAgent = new AIParrelAgent(aiPlanAgent);\n\nAISequenceAgent innerSequence = new AISequenceAgent(aiPlanAgent);\ninnerSequence.addAgent(new AINodeAgent(\"步骤1\"));\ninnerSequence.addAgent(new AINodeAgent(\"步骤2\"));\n\nparrelAgent.addAgent(innerSequence);\nparrelAgent.addAgent(new AINodeAgent(\"独立任务\"));\n\naiPlanAgent.addAgent(parrelAgent);\n```\n\n### 14.10 智能体工作流变量体系\n\n智能体工作流支持丰富的变量传递机制,支持在智能体之间传递数据,并在 Prompt 中动态引用变量。\n\n#### 14.10.1 变量概述\n\n工作流中的变量用于实现智能体之间的数据共享和传递,主要包含以下使用场景:\n\n- **输出变量**:将某个智能体的执行结果保存为变量,供后续节点使用\n- **Prompt 变量引用**:在提示词中通过占位符动态注入其他节点的输出结果\n- **内置变量**:框架自动注入的流程级变量,如用户输入、系统提示等\n- **上下文变量**:通过 `JobFlowNodeExecuteContext` 或 `NodeTriggerContext` 在代码层面读写\n- **智能体参数变量**:通过Agent.addParam(key,value)方式添加,主要用于非工作流场景,添加的变量,可以智能体提示词和系统提示词中使用\n\n#### 14.10.2 设置输出变量\n\n通过 `setOutputVaribleName` 方法将智能体的输出结果保存为指定名称的变量,并可指定作用域:\n\n```java\nimport org.frameworkset.spi.ai.model.AIFlowConst;\n\naiPlanAgent.addAgent(new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setOutputVaribleName(\"provinces\", AIFlowConst.AIFLOW_VAR_SCOPE_FLOW)\n .setAgentName(\"介绍中国省份和直辖市\")\n .setAgentId(\"introduceProvinces\"));\n```\n\n| 参数 | 说明 |\n|------|------|\n| `varName` | 变量名称,后续通过该名称引用 |\n| `scope` | 变量作用域,可选 `AIFlowConst.AIFLOW_VAR_SCOPE_FLOW`(全局)、`AIFLOW_VAR_SCOPE_NODE`(节点级)、`AIFLOW_VAR_SCOPE_CONTAINER`(容器级) |\n\n#### 14.10.3 变量作用域\n\n| 作用域常量 | 说明 |\n|-----------|------|\n| `AIFLOW_VAR_SCOPE_FLOW` | 全局作用域,整个工作流均可访问 |\n| `AIFLOW_VAR_SCOPE_NODE` | 节点作用域,仅在当前节点内有效 |\n| `AIFLOW_VAR_SCOPE_CONTAINER` | 容器作用域,仅在当前串行/并行容器内有效 |\n\n#### 14.10.4 在 Prompt 中使用变量\n\n在智能体的提示词中,通过 `#[变量名,scope=作用域]` 的语法引用已定义的变量:\n\n```java\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent)\n .setAgentId(\"aiParrelAgent\")\n .setAgentName(\"共享任务节点\");\n\n// 在 Prompt 中引用 flow 作用域的 provinces 变量\naiParrelAgent.addAgent(new AINodeAgent(\n \"同时结合中国省份特点:\\r\\n#[provinces,scope=flow],\\r\\n用300字介绍湖南\")\n .setAgentId(\"jieshaohunan\")\n .setAgentName(\"用50字介绍湖南\"));\n```\n\n**变量引用语法:**\n\n```\n#[变量名,scope=作用域]\n```\n\n其中 `scope` 可选值为:`flow`、`node`、`container`,需与定义变量时指定的作用域一致。\n\n#### 14.10.5 在代码中获取变量\n\n在条件触发器或通用流程节点中,可通过上下文对象获取变量值:\n\n**条件触发器中获取变量:**\n```java\naiPlanAgent.addAgent(new AINodeAgent(\"将结果创建为飞书文档\", feishuMcp)\n .setAgentId(\"createDocAgent\")\n .setAgentName(\"飞书文档创建智能体\"),\n nodeTriggerContext -> {\n // 获取 judgeAgent 智能体的裁判结果\n String judgeResult = (String) nodeTriggerContext\n .getFlowContextData(\"judgeAgent.judgeResult\");\n return \"是\".equals(judgeResult);\n });\n```\n\n**通用流程节点中获取变量:**\n```java\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n Object data = jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\");\n return data;\n }\n});\n```\n\n#### 14.10.6 内置变量\n\n框架自动维护以下内置变量:\n\n| 内置变量 | 说明 |\n|---------|------|\n| `input.query` | 用户输入的原始问题(Prompt) |\n| `input.system` | 系统提示词(System Prompt) |\n| `judgeAgent.judgeResult` | `AIJudgeAgent` 裁判节点的评估结果 |\n\n#### 14.10.8 智能体参数变量\n\n在非工作流智能体中,亦可以使用外部资源变量和智能体参数变量,示例如下\n\n智能体参数变量:\n\n```java\n //定义提示词\n String message = \"介绍一下bboss jobflow #[version]\";\nAIAgent aiAgent = new AIAgent();\naiAgent.addParam(\"version\",\"7.5.7\");//添加version参数,用于在问题中替换#[version]\n```\n\n外部资源:参考章节[14.11 加载外部资源提示词](#chapter1411)\n\n#### 14.10.7 完整示例\n\n以下示例演示了从变量定义、Prompt 引用到代码获取的完整流程:\n\n```java\n// 1. 定义输出变量:将省份介绍结果保存为 flow 级变量\naiPlanAgent.addAgent(new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setOutputVaribleName(\"provinces\", AIFlowConst.AIFLOW_VAR_SCOPE_FLOW)\n .setAgentId(\"introduceProvinces\"));\n\n// 2. 在并行智能体中通过 Prompt 引用变量\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent);\naiParrelAgent.addAgent(new AINodeAgent(\n \"结合省份特点:#[provinces,scope=flow],用300字介绍湖南\")\n .setAgentId(\"jieshaohunan\"));\n\n// 3. 通用流程节点处理变量\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext ctx) {\n String provinces = (String) ctx.getJobFlowContextData(\"provinces\");\n logger.info(\"获取到省份数据:{}\", provinces);\n ctx.addJobFlowContextData(\"processed\", true);\n return null;\n }\n});\n```\n\n### 14.11 加载外部资源提示词\n\n<a id=\"chapter1411\"></a>\n\n智能体支持三种加载外部资源提示词的方法:\n\n1)加载外部文件中的提示词\n\n2)加载classpath下文件中的提示词\n\n3)加载url中提供的提示词\n\n分别举例如下:\n\n```java\n String message = \"#[prompt.txt,type=resource,charset=UTF-8]\";\n String message = \"#[prompt.txt,type=resource]\";\n String message = \"#[http://localhost:85/prompt.txt,type=url,charset=UTF-8]\";\n String message = \"#[C:\\\\workspace\\\\bbossgroups\\\\bboss-ai\\\\bboss-ai-flow\\\\src\\\\test\\\\resources\\\\prompt.txt,type=file,charset=UTF-8]\";\n```\n\n参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-prompt-resource\n\n### 14.12 流程调度\n\nbboss提供了智能体流程一次性执行和定期调度执行机制,可以设置节假日忽略执行策略,先看一个完整示例:\n\n```java\nprivate static void initDeepseekService(){\n \n HttpRequestProxy.startHttpPools(\"application.properties\");//启动模型服务服务\n\n }\n public static void main(String[] args){\n //初始化Deepseek服务\n initDeepseekService();\n ChatAgentMessage chatAgentMessage = new ChatAgentMessage();\n chatAgentMessage.setModel(\"deepseek-chat\").setMaas(\"deepseek\");\n\n //构建流程\n AIPlanAgent planAgent = new AIPlanAgent(new StoreContext().setStoreType(StoreContext.STORE_TYPE_MEMORY).setSessionSize(100));\n planAgent.setAgentName(\"Deepseek写诗-评价诗词流程\")\n .setAgentId(\"测试id\").setAgentMessage(chatAgentMessage);\n \n HolidayJobFlowScheduleConfig jobFlowScheduleConfig = new HolidayJobFlowScheduleConfig();\n jobFlowScheduleConfig.setDelay(1000L);//延期1秒执行\n jobFlowScheduleConfig.setFixedRate(true);\n jobFlowScheduleConfig.setPeriod(30000L);//每隔30秒执行\n jobFlowScheduleConfig.addCustomHoliday(\"2026-06-10\");//设定某天不执行,可以调用多次添加多天不执行\n \n planAgent.setJobFlowScheduleConfig(jobFlowScheduleConfig);\n\n planAgent.addAgent(new AINodeAgent(\"模仿李白的风格写一首七律.飞机!\").setSystemPrompt(\"你是一位唐代诗人.\").setAgentId(\"1\").setAgentName(\"Deepseek写诗-评价诗词流程\").setAgentOutput(new AgentOutput() {\n @Override\n public void output(ServerEvent message) {\n logger.info(\"--------诗歌内容---------\\n{}\",message.getData());\n }\n }));\n\n planAgent.addAgent(new AINodeAgent(\"帮忙评估上述诗词的意境\").setAgentId(\"2\").setAgentName(\"Deepseek-chat-分析诗\").setAgentOutput(new AgentOutput() {\n @Override\n public void output(ServerEvent message) {\n logger.info(\"--------诗歌评价---------\\n{}\",message.getData());\n }\n }));\n\n\n planAgent.chat();\n \n \n\n }\n```\n\nbboss默认采用一次性执行智能体编排工作流,如需周期性执行,则进行以下配置:\n\n\n```java\n HolidayJobFlowScheduleConfig jobFlowScheduleConfig = new HolidayJobFlowScheduleConfig();\n jobFlowScheduleConfig.setDelay(1000L);//延期1秒执行\n jobFlowScheduleConfig.setFixedRate(true);\n jobFlowScheduleConfig.setPeriod(30000L);//每隔30秒执行\n jobFlowScheduleConfig.addCustomHoliday(\"2026-06-10\");//设定某天不执行,可以调用多次添加多天不执行\n jobFlowScheduleConfig.setSkipSunday(true);\n planAgent.setJobFlowScheduleConfig(jobFlowScheduleConfig);\n```\n如果需要跳过节假日不执行,则进行如下设置:\n\n```java\njobFlowScheduleConfig.addCustomHoliday(\"2026-06-10\");//设定某天不执行,可以调用多次添加多天不执行\n/**\n * 设置跳过星期六\n \n */\njobFlowScheduleConfig.skipSaturday() \n\n/**\n * 设置跳过星期天\n * @return\n */\njobFlowScheduleConfig.skipSunday() \n\n/**\n * 设置跳过周末(星期六和星期天)\n * @return\n */\njobFlowScheduleConfig.skipWeekends() \n\n/**\n * 设置跳过元旦(1月1日)\n * @return\n */\njobFlowScheduleConfig.skipNewYearsDay() \n\n/**\n * 设置跳过劳动节(5月1日)\n * @return\n */\njobFlowScheduleConfig.skipLaborDay() \n\n/**\n * 设置跳过端午节\n * @return\n */\njobFlowScheduleConfig.skipDragonBoatFestival() \n\n/**\n * 设置跳过中秋节\n * @return\n */\njobFlowScheduleConfig.skipMidAutumnFestival() \n\n/**\n * 设置跳过国庆节(10月1日)\n * @return\n */\njobFlowScheduleConfig.skipNationalDay() \n\n/**\n * 设置跳过春节\n * @return\n */\njobFlowScheduleConfig.skipSpringFestival() \n\n/**\n * 设置跳过所有内置节假日(元旦、劳动节、端午节、中秋节、国庆节、春节)及周末\n * @return\n */\njobFlowScheduleConfig.skipAllHolidays() \n```\n\n### 14.13 自定义消息推送\n\n```java\nServerEvent serverEvent = new ServerEvent();//向客户端推送拒答信息\nserverEvent.setType(ServerEvent.TYPE_REFUSAL);\nserverEvent.setData(msg);\nserverEvent.setConfidence(confidence);\nserverEvent.setDone(true);//告诉前端流已经输出完毕\ngetAgentFluxSink().next(serverEvent);\n```\n\n### 14.14 流程轨迹记录\n\n```java\n// ===== 2. 加载历史会话消息,如果会话不存在,则创建 =====\n\t\t\t\t\tplanAgent.loadSessionMemory( question, domain);\n\t\t\t\t\tTraceMessage traceMessage = new TraceMessage();\t\t\n\t\t\t\t\t//记录用户输入的原始问题\n\t\t\t\t\ttraceMessage.setMessage(Map.of(\"question\", question,\"role\",SessionMessage.MESSAGE_TYPE_USERINPUTMESSAGE_NAME));\n\t\t\t\t\t//其他用户上传的附件材料信息可以放到metaData中,也可以直接放到上面的消息中\n//\t\t\t\t\ttraceMessage.setMetaData(Map.of(\"documents\", new ArrayList<>()));\n\t\t\t\t\ttraceMessage.setStartTime(System.currentTimeMillis());\n//\t\t\t\t\ttraceMessage.setEndTime(System.currentTimeMillis());\n\t\t\t\t\tplanAgent.recordTraceMessage(traceMessage);\n```\n\n\n\n### 14.15 停止智能体工作流\n\n可以通过以下方法停止智能体工作流的执行:\n\n```java\naiPlanAgent.shutdown();\n```\n\n## 十五、向量模型与Rerank\n\nbboss AI 内置了文本向量化(Embedding)和重排序(Rerank)能力,可配合 Elasticsearch 等向量数据库实现检索增强生成(RAG)。\n\n### 15.1 向量模型 Embedding\n\nbboss-ai 内置了统一的向量模型调用能力,通过 `AIAgent.embedding(EmbeddingMessage)` 方法即可将文本转换为高维向量。\n\n#### 基本用法\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.EmbeddingMessage;\n\npublic float[] text2embedding(String text) {\n EmbeddingMessage embeddingMessage = new EmbeddingMessage();\n embeddingMessage.setInput(text); // 设置将要向量化的数据\n embeddingMessage.setModel(\"bge-m3\"); // 指定向量模型名称\n embeddingMessage.setMaas(\"embedding_model\"); // 指定 MaaS 平台服务名\n\n AIAgent agent = new AIAgent();\n float[] embedding = agent.embedding(embeddingMessage);\n if (embedding == null) {\n throw new AIRuntimeException(\"Embedding failed: response is null\");\n }\n return embedding;\n}\n```\n\n#### 关键参数说明\n\n| 参数 | 说明 |\n|------|------|\n| `input` | 待向量化的文本内容 |\n| `model` | 向量模型名称,如 `bge-m3`、`bge-large-zh-v1.5` 等 |\n| `maas` | MaaS 平台服务配置名,对应 `application.properties` 中的服务定义 |\n\n> **提示**:向量模型服务需在 `application.properties` 中预先配置好对应的 MaaS 服务地址和认证信息。\n\n---\n\n### 15.2 Rerank 重排序\n\n在 RAG(检索增强生成)场景中,向量检索和 BM25 文本检索返回的结果往往存在排序差异。`AIAgent.rerank(RerankMessage)` 方法调用专业的 Rerank 模型对候选文档进行重新排序,显著提升检索结果的相关性。\n\n#### 基本用法\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.RerankMessage;\nimport org.frameworkset.spi.ai.model.RerankDocument;\nimport org.frameworkset.spi.ai.model.RerankedDocument;\n\npublic List<RerankedDocument> rerankDocuments(String query, List<RerankDocument> candidates) {\n RerankMessage rerankMessage = new RerankMessage();\n rerankMessage.setMaas(\"agw\"); // MaaS 服务名\n rerankMessage.setModel(\"bge-reranker-v2-m3\"); // Rerank 模型\n rerankMessage.setRerankDocuments(candidates); // 候选文档列表\n rerankMessage.setQuery(query); // 用户查询\n rerankMessage.setReturnDocuments(false); // 是否返回原始文本\n rerankMessage.setTopK(10); // 最多返回前 10 个最相关文档\n rerankMessage.setRelevanceScore(0.5d); // 只召回相似度大于该值的文档\n\n AIAgent aiAgent = new AIAgent();\n List<RerankedDocument> rerankedDocuments = aiAgent.rerank(rerankMessage);\n return rerankedDocuments;\n}\n```\n#### RerankMessage 数据结构\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `maas` | `String` | MaaS 平台服务配置名,对应 `application.properties` 中的服务定义 |\n| `model` | `String` | Rerank 模型名称,如 `bge-reranker-v2-m3` 等 |\n| `query` | `String` | 用户查询语句,用于与候选文档计算相关性 |\n| `rerankDocuments` | `List<RerankDocument>` | 候选文档列表,需预先通过向量检索或 BM25 检索构建 |\n| `returnDocuments` | `boolean` | 是否在结果中返回原始文档内容;设为 `true` 可在结果中直接获取原始文本,设为 `false` 则仅返回分数和索引,减少数据传输。 |\n| `topK` | `int` | 最多返回的文档数量,只召回相似度最高的前 `topK` 个文档 |\n| `relevanceScore` | `double` | 相关性分数阈值,只召回相似度大于该值的文档 |\n\n#### RerankDocument 数据结构\n\n```java\nRerankDocument doc = new RerankDocument();\ndoc.setDocument(\"文档原始文本内容\"); // 设置文档内容\ndoc.setVectorScore(0.85f); // 来自向量检索的分数(可选)\ndoc.setBm25Score(12.5f); // 来自 BM25 检索的分数(可选)\ndoc.setMetadata(metaMap); // 附加元数据(可选)\n```\n\n#### RerankedDocument 返回结构\n\n| 字段 | 说明 |\n|------|------|\n| `index` | 原始文档在候选列表中的位置 |\n| `relevanceScore` | Rerank 模型计算的相关性分数 |\n| `document` | 原始文档内容(当 `returnDocuments=true` 时返回) |\n\n\n---\n\n### 15.3 RAG 实战:混合检索 + Rerank\n\n以下是一个完整的 RAG 检索流程示例,演示如何结合 Elasticsearch KNN 向量检索、BM25 文本检索和 Rerank 重排序,实现高质量的知识库问答。\n\n#### 15.3.1 初始化与启动\n\n```java\nimport org.frameworkset.elasticsearch.boot.ElasticSearchBoot;\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\n\n// 启动 Elasticsearch 客户端和 HTTP 连接池\nElasticSearchBoot.boot(\"application-stream.properties\");\nHttpRequestProxy.startHttpPools(\"application-stream.properties\");\n```\n\n#### 15.3.2 完整检索流程\n\n```java\nimport org.frameworkset.elasticsearch.ElasticSearchHelper;\nimport org.frameworkset.elasticsearch.client.ClientInterface;\nimport org.frameworkset.elasticsearch.entity.ESDatas;\nimport org.frameworkset.elasticsearch.entity.MetaMap;\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.*;\n\npublic void searchVectorAndRerank() {\n ClientInterface clientUtil = ElasticSearchHelper\n .getConfigRestClientUtil(\"esmapper/knowledge.xml\");\n String query = \"Spring AOP\";\n\n // 1. 向量检索(KNN)\n Map vectorParams = new LinkedHashMap();\n vectorParams.put(\"embedding\", text2embedding(query));\n vectorParams.put(\"k\", 100);\n vectorParams.put(\"size\", 50);\n vectorParams.put(\"similarity\", 0.5);\n ESDatas<MetaMap> vectorDatas = clientUtil.searchList(\n \"/knowledge_chunks/_search\", \"searchWithScore\",\n vectorParams, MetaMap.class);\n List<MetaMap> vectorResults = vectorDatas.getDatas();\n\n // 2. BM25 文本检索\n Map bm25Params = new LinkedHashMap();\n bm25Params.put(\"query\", query);\n bm25Params.put(\"size\", 50);\n bm25Params.put(\"is_active\", true);\n ESDatas<MetaMap> bm25Datas = clientUtil.searchList(\n \"/knowledge_chunks/_search\", \"searchBM25\",\n bm25Params, MetaMap.class);\n List<MetaMap> bm25Results = bm25Datas.getDatas();\n\n // 3. 合并结果(去重)\n Map<String, RerankDocument> mergedMap = new LinkedHashMap<>();\n if (vectorResults != null) {\n for (MetaMap metaMap : vectorResults) {\n String chunkId = (String) metaMap.get(\"chunk_id\");\n RerankDocument doc = new RerankDocument();\n doc.setDocument((String) metaMap.get(\"content\"));\n doc.setVectorScore(metaMap.getScore());\n doc.setMetadata(metaMap);\n mergedMap.put(chunkId, doc);\n }\n }\n if (bm25Results != null) {\n for (MetaMap metaMap : bm25Results) {\n String chunkId = (String) metaMap.get(\"chunk_id\");\n RerankDocument doc = mergedMap.get(chunkId);\n if (doc == null) {\n doc = new RerankDocument();\n doc.setDocument((String) metaMap.get(\"content\"));\n doc.setBm25Score(metaMap.getScore());\n doc.setMetadata(metaMap);\n mergedMap.put(chunkId, doc);\n } else {\n doc.setBm25Score(metaMap.getScore());\n }\n }\n }\n\n // 4. Rerank 重排序\n if (mergedMap != null && mergedMap.size() > 0) {\n List<RerankDocument> rerankDatas = new ArrayList<>(mergedMap.values());\n\n RerankMessage rerankMessage = new RerankMessage();\n rerankMessage.setMaas(\"agw\");\n rerankMessage.setModel(\"bge-reranker-v2-m3\");\n rerankMessage.setRerankDocuments(rerankDatas);\n rerankMessage.setQuery(query);\n rerankMessage.setReturnDocuments(false);\n rerankMessage.setTopK(3); // 最多返回前 3 个最相关文档\n rerankMessage.setRelevanceScore(0.8d); // 只召回相似度大于 0.8 的文档\n\n AIAgent aiAgent = new AIAgent();\n List<RerankedDocument> rerankedDocuments = aiAgent.rerank(rerankMessage);\n\n // 按 relevanceScore 排序后的最终结果就是高质量检索结果\n logger.info(\"Rerank 结果: {}\", JsonUtil.object2jsonPretty(rerankedDocuments));\n }\n}\n```\n\n#### 流程说明\n\n1. **向量检索**:通过 KNN 搜索获取语义相关的文档片段\n2. **BM25 检索**:通过关键词匹配获取文本相关的文档片段\n3. **结果合并**:以 `chunk_id` 为键去重合并,同时保留两种检索的分数\n4. **Rerank 排序**:调用 Rerank 模型对合并后的候选集进行精细排序,返回按相关性排列的最终结果\n\n> **混合检索方案**:Elasticsearch 8.x 也支持原生的混合检索(`knn` + `query` + `rank: { rrf }`),可通过 `searchHybrid` DSL 一步完成向量+BM25+RRF 融合,详见下面索引配置中的 `searchHybrid` 定义。\n\n---\n\n### 15.4 Elasticsearch 索引配置\n\n以下是为知识库检索场景设计的 Elasticsearch 索引模板和检索 DSL 配置,需放置在 `esmapper/knowledge.xml` 中。\n\n#### 15.4.1 索引结构与 Mapping\n\n```xml\n<property name=\"createKnowledgeChunksIndex\">\n <![CDATA[{\n \"settings\": {\n \"number_of_shards\": 1,\n \"number_of_replicas\": 0,\n \"analysis\": {\n \"filter\": {\n \"cn_stop\": {\n \"type\": \"stop\",\n \"stopwords\": [\"的\", \"了\", \"是\", \"在\", \"和\", \"或\", \"与\", \"对\", \"中\", \"也\", \"都\", \"就\", \"而\"]\n }\n },\n \"analyzer\": {\n \"cn_index_analyzer\": {\n \"type\": \"custom\",\n \"tokenizer\": \"ik_max_word\",\n \"filter\": [\"lowercase\", \"cn_stop\"]\n },\n \"cn_search_analyzer\": {\n \"type\": \"custom\",\n \"tokenizer\": \"ik_smart\",\n \"filter\": [\"lowercase\", \"cn_stop\"]\n }\n }\n }\n },\n \"mappings\": {\n \"properties\": {\n \"chunk_id\": { \"type\": \"keyword\" },\n \"document_id\": { \"type\": \"keyword\" },\n \"batch_id\": { \"type\": \"keyword\" },\n \"is_active\": { \"type\": \"boolean\" },\n \"doc_path\": { \"type\": \"keyword\" },\n \"anchor\": { \"type\": \"keyword\" },\n \"domain\": { \"type\": \"keyword\" },\n \"doc_type\": { \"type\": \"keyword\" },\n \"heading_path\": {\n \"type\": \"text\",\n \"analyzer\": \"cn_index_analyzer\",\n \"search_analyzer\": \"cn_search_analyzer\"\n },\n \"content\": {\n \"type\": \"text\",\n \"analyzer\": \"cn_index_analyzer\",\n \"search_analyzer\": \"cn_search_analyzer\",\n \"fields\": {\n \"en\": { \"type\": \"text\", \"analyzer\": \"english\" }\n }\n },\n \"embedding\": {\n \"type\": \"dense_vector\",\n \"dims\": 1024,\n \"index\": true,\n \"similarity\": \"cosine\"\n },\n \"created_at\": { \"type\": \"date\" }\n }\n }\n }]]>\n</property>\n```\n\n#### 15.4.2 KNN 向量检索 DSL\n\n```xml\n<property name=\"searchWithScore\">\n <![CDATA[{\n \"size\":#[size],\n \"knn\": {\n \"field\": \"embedding\",\n \"query_vector\": #[embedding,serialJson=true],\n \"k\": #[k],\n \"similarity\": #[similarity],\n \"num_candidates\": 100,\n \"boost\": 0.9\n }\n }]]>\n</property>\n```\n\n#### 15.4.3 BM25 文本检索 DSL\n\n```xml\n<property name=\"searchBM25\">\n <![CDATA[{\n \"size\": #[size],\n \"query\": {\n \"bool\": {\n \"must\": [\n {\n \"multi_match\": {\n \"query\": #[query],\n \"fields\": [\"content^3\", \"heading_path^2\"],\n \"type\": \"best_fields\"\n }\n }\n ],\n \"filter\": [\n { \"term\": { \"is_active\": #[is_active] } }\n ]\n }\n }\n }]]>\n</property>\n```\n\n#### 15.4.4 混合检索(KNN + BM25 + RRF)DSL\n\n```xml\n<property name=\"searchHybrid\">\n <![CDATA[{\n \"size\": #[size],\n \"query\": {\n \"bool\": {\n \"must\": [\n {\n \"multi_match\": {\n \"query\": #[query],\n \"fields\": [\"content^3\", \"heading_path^2\"],\n \"type\": \"best_fields\"\n }\n }\n ],\n \"filter\": [\n { \"term\": { \"is_active\": #[is_active] } }\n ]\n }\n },\n \"knn\": {\n \"field\": \"embedding\",\n \"query_vector\": #[embedding,serialJson=true],\n \"k\": #[k],\n \"similarity\": #[similarity],\n \"num_candidates\": 200,\n \"boost\": 0.5,\n \"filter\": [\n { \"term\": { \"is_active\": #[is_active] } }\n ]\n },\n \"rank\": {\n \"rrf\": {\n \"window_size\": 100,\n \"rank_constant\": 60\n }\n }\n }]]>\n</property>\n```\n\n#### 索引配置要点\n\n| 配置项 | 说明 |\n|--------|------|\n| `dense_vector` | `embedding` 字段类型,`dims: 1024` 对应 bge-m3 模型输出维度 |\n| `similarity: cosine` | 向量相似度算法,可选 `cosine`、`dot_product`、`l2_norm` |\n| `ik_max_word` / `ik_smart` | IK 分词器的索引模式和搜索模式 |\n| `cn_stop` | 自定义中文停用词过滤 |\n| `RRF` | Reciprocal Rank Fusion,Elasticsearch 原生多路检索融合算法 |\n\n> **注意**:使用 `searchHybrid` 需要 Elasticsearch 8.x 及以上版本,且索引的 `dense_vector` 字段必须设置 `index: true`。\n\n---\n\n## 十六 智能体 Trace 可观测性\n\nbboss-ai 内置了一套全链路、多维度的智能体 Trace 可观测性体系,覆盖 LLM 调用、工具执行、工作流编排等全部环节,支持内存和数据库、Clickhouse三中持久化方式,并可通过流式通道实时推送观测事件到前端。\n\n### **16.1 核心模型**\n\n| 类名 | 作用 |\n| --------------------------- | ------------------------------------------------------------ |\n| `TraceMessage` | Trace 消息载体,包含消息内容、起止时间、agentId、parentAgentId、traceId、metaData 等 |\n| `TokenMetrics` | Token 用量指标,包含 model、totalTokens、promptTokens、completionTokens、completionReasoningTokens、reasoningData、elapsed 等 |\n| `SessionMessage` | 会话消息类型体系,定义了 18 种消息类型常量,覆盖用户输入、LLM 输入输出、Embedding、Rerank、工具搜索、MCP 调用、Trace 等 |\n| `AgentMessageTypeConvertor` | 角色(role)到 messageType 的转换器,支持用户自定义扩展(101 起) |\n<a id=\"chapter16-2\"></a>\n\n### 16.2 消息类型对照表\n\n| 常量 | 值 | role 名称 | 说明 |\n| -------------------------------------- | ---- | --------------- | ------------------------------------------------------------ |\n| `MESSAGE_TYPE_ASSISTANT_MESSAGE` | 0 | assistant | 智能体辅助消息(如果是tool,无需在会话历史中呈现,因为需要通过toolsearch类型在历史中展示),大模型回复消息 |\n| `MESSAGE_TYPE_TOOL_MESSAGE_NAME` | 4 | tool | 智能体辅助消息:工具调用结果消息,需要提交给大模型 |\n| `MESSAGE_TYPE_AGENT_RESULTMESSAGE` | 1 | agentresult | 智能体输出结果 |\n| `MESSAGE_TYPE_USER_MESSAGE` | 2 | user | 用户输入消息(提交给模型)用户提问 |\n| `MESSAGE_TYPE_SYSTEM_MESSAGE` | 3 | system | 系统消息 |\n| `MESSAGE_TYPE_TRACE_MESSAGE` | 5 | trace | 智能体跟踪消息 |\n| `MESSAGE_TYPE_RAG_MESSAGE` | 6 | rag | RAG 知识库资料消息 |\n| `MESSAGE_TYPE_REFUSE_MESSAGE` | 7 | refuse | 拒答消息 |\n| `MESSAGE_TYPE_USER_INPUTMESSAGE` | 8 | userinput | 用户原始输入(含文件、图片描述) |\n| `MESSAGE_TYPE_LLM_INPUTMESSAGE` | 9 | llminput | 提交给 LLM 的完整请求报文 |\n| `MESSAGE_TYPE_LLM_OUTPUTMESSAGE` | 10 | llmoutput | LLM 返回的完整响应报文 |\n| `MESSAGE_TYPE_EMBEDDING_INPUTMESSAGE` | 11 | embeddinginput | Embedding 模型输入 |\n| `MESSAGE_TYPE_RERANK_INPUTMESSAGE` | 12 | rerankinput | Rerank 模型输入 |\n| `MESSAGE_TYPE_EMBEDDING_OUTPUTMESSAGE` | 13 | embeddingoutput | Embedding 模型输出 |\n| `MESSAGE_TYPE_RERANK_OUTPUTMESSAGE` | 14 | rerankoutput | Rerank 模型输出 |\n| `MESSAGE_TYPE_TOOLSEARCH_MESSAGE` | 15 | toolsearch | 工具搜索匹配消息(会话历史中需要展示) |\n| `MESSAGE_TYPE_MCPCALL_MESSAGE` | 16 | mcpcall | MCP 服务调用消息 |\n| `MESSAGE_TYPE_OTHER_MESSAGE` | 18 | other | 智能体工具服务调用消息:其他消息类型,定义的消息名称(role)没有设置对应的编码,返回18 |\n| `MESSAGE_TYPE_TOOLCALL_MESSAGE` | 17 | toolcall | 工具服务调用消息 |\n| MESSAGE_TYPE_SKILLCALL_MESSAGE | 19 | skillcall | 智能体Skill调用消息 |\n| MESSAGE_TYPE_HITL_MESSAGE | 20 | hitltask | 智能体人工介入消息:大模型提供输入内容,需要人工介入处理 |\n| MESSAGE_TYPE_HITL_HANDLE_MESSAGE | 21 | hitlhandle | 智能体人工介入处理消息:人工介入处理完成后,返回给大模型的消息和数据内容 |\n\n### **16.3 自动 Trace 采集**\n\n`AIAgentUtil` 在各类模型调用前后自动插入 Trace,无需业务代码介入:\n\n- `traceLLMInput()`:记录提交给模型的完整请求报文(LLM、Embedding、Rerank)\n- `traceLLMOutput()`:记录模型返回的完整响应报文\n- 覆盖场景:同步/流式聊天、图片生成、音频生成、视频任务提交、Embedding、Rerank\n\n### **16.4 流式 Token 计量**\n\n`BaseStreamDataBuilder` 在流式响应过程中通过 `computeTokens()` 累加各数据段的 Token 用量,最终在 `addChatWithToolCallSessionMessage()` 中将完整结果与 `TokenMetrics` 一并持久化。\n\n### **16.5 手动 Trace 记录**\n\n- **`AIAgent.recordTraceMessage()`**:智能体入口方法,自动补全 agentId 和 parentAgentId,写入主会话存储\n- **`AgentTraceHolder`**:基于 ThreadLocal 的 Trace 上下文持有者,支持在工具调用、异步执行等跨线程场景中安全记录 Trace;提供 `trace()` 和 `emitterServerEvent()` 方法\n- **`AIFlowNode.recordTraceMessage()` / `AIFlowNodeVoid.recordTraceMessage()`**:工作流普通节点支持手动记录 Trace,自动绑定 nodeId 和 parentAgentId\n\n### **16.6 工作流编排 Trace**\n\n工作流引擎在关键决策点自动记录 Trace:\n\n- **`AIRouterNodeBuilder`**:AI 路由节点记录路由选择结果(匹配成功/失败、重试次数)\n- **`AIKeywordsRouterNodeBuilder`**:关键词路由记录匹配到的智能体 ID 及描述\n- **自定义节点**:业务可通过 `recordTraceMessage()` 记录循环控制、条件判断等自定义轨迹(如循环次数、修复标记)\n\n### **16.7 实时流式推送**\n\n`ServerEvent` 支持 `TYPE_TRACE = 2` 类型,可将 Trace 信息通过 Flux/SSE 实时推送给前端。流式场景下,路由失败、重试等关键事件可即时反馈到用户界面。\n\n### **16.8 存储与持久化**\n\n```\nAgentSessionStore(接口)\n ├── AgentSessionStoreMemory(内存存储)\n └── AgentSessionStoreDB(数据库存储)\n └── agent_session_message 表\n```\n\n`AgentSessionStoreDB` 自动完成建表,核心字段包括:msgId、createTime、sessionId、parentAgentId、agentId、agentNodeType、subAgentIdBy、messageType、seqNo、message(JSON)、role、marks、metadata、requestId、tokenMetrics(JSON)、elapsed、traceId。\n\nTrace 消息与业务消息统一存储在同一张表中,通过 `messageType` 和 `role` 区分,便于按会话维度进行全链路回放和分析,例如:`messageType = 5` 和 `role = trace` 。具体消息类型映射关系,看上面的对照表。\n\n\n\n## 十七、技术支持\n\n- **技术交流群**:21220580、166471282\n\n- **参考文档**:https://doc.bbossgroups.com/#/mvc/chatstream\n\n- **多模态智能问答web demo**:https://gitee.com/bboss/bbootdemo\n 关键代码:\n\n 流式问答控制器 https://gitee.com/bboss/bbootdemo/blob/master/src/main/java/org/frameworkset/web/react/ReactorController.java\n\n 前端网页 https://gitee.com/bboss/bbootdemo/blob/master/WebRoot/chatBackuppressSession.html\n\n\n- **后端多模态演示案例**:https://gitee.com/bboss/bboss-ai\n\n 关键代码:https://gitee.com/bboss/bboss-ai/blob/main/bboss-ai/src/test/java/org/frameworkset/spi/ai/StreamTest.java"}, "425151586125646dce4eed5a1bbcfe73bd83fb24": {"t": 1785809903.8839111, "n": 111731, "h": "4977324348d69056", "c": "# 多模态AI智能体开发框架使用教程\n\n## 一、概述\n\n**bboss-ai** 是一个轻量级 Java AI Agent 开发客户端,基于 Apache HttpClient5、HttpCore5 以及 Project Reactor 构建,提供了对大语言模型(LLM)和多模态模型的统一对接能力,支持同步调用和流式调用两种模式,并内置智能体工作流编排、会话管理、工具搜索等企业级特性。能够快速集成各大主流 AI 模型平台,实现智能问答、图片识别/生成、语音识别/生成、视频识别/生成等功能。\n\n**ClickHouse 生产级会话存储**:bboss-ai 支持基于 ClickHouse 分布式集群的生产级会话持久化能力。使用时需要指定 ClickHouse 集群名称,并为每个集群节点定义名为 `shard` 和 `replica` 的两个宏变量。ClickHouse 模式下会话续问续答时不会更新最后访问时间(受限于 ClickHouse 不支持高频 UPDATE)。详细使用方式参见 [Clickhouse会话存储管理](#chatper14-122) 章节。\n\n### 核心功能\n\n- **模型能力**:兼容 DeepSeek、Kimi、智谱、阿里百炼通义千问、字节豆包火山引擎、MiniMax、腾讯混元、中国移动九天、chatgpt等主流 MaaS 平台;通过简单的适配和扩展即可支持私有化模型平台\n\n- **流式响应**:基于 Reactor 的响应式编程模型,支持背压控制机制\n\n- **工具能力**:支持工具调用和 MCP 服务发现,提供 MCP SSE 和 Streamable 两种通讯协议,同时提供mcp server协议实现\n\n - 工具调用(Function Calling)\n - MCP(Model Context Protocol)服务发现和调用,支持 SSE 和 Streamable HTTP 两种传输模式\n - 可快速发布供LLM调用的工具服务和MCP服务\n - 多轮工具调用(Loop Tool Call),支持智能体自主决策多步骤任务执行\n - 脚本执行工具(Shell/CLI)和代码执行工具(Python/NodeJS)\n - Skills 技能模块,支持技能注册、加载、调度执行及可观测性监测\n - 工具搜索(Tool Searcher),支持基于关键词和语义的工具过滤\n\n- **多模态支持**:支持文本、图片、音频、视频等多种模态的识别与生成\n\n - 图片识别与生成(Vision/Image Generation)\n - 语音识别与合成(Speech-to-Text/Text-to-Speech)\n - 视频识别与生成(Video Understanding/Generation)\n\n- **检索增强生成(RAG):**内置文本向量化(Embedding)和重排序(Rerank)能力,支持知识库检索增强生成(RAG)\n\n - 向量嵌入(Embedding)\n - 重排序(Rerank)\n - 支持Elasticsearch、Milvus主流向量数据库\n\n- **多智能体协同**:基于有向循环图实现多智能体协同工作流,支持串行、并行、路由、条件分支、裁判评估等丰富的流程编排能力,快速构建复杂多智能体系统\n\n \n\n - 多智能体编排工作流底层基于[bboss jobflow](https://esdoc.bbossgroups.com/#/jobworkflow)实现(一套数据交换作业编排工作流)\n\n- **多轮会话**:支持会话长短期记忆管理,会话消息智能压缩;支持多智能体协同通讯和信息共享;子智能体独立会话空间,彼此隔离,互不干扰;支持内存、关系数据库(MySQL、Oracle、达梦 DM、SQL Server、PostgreSQL、SQLite)、Clickhouse多种会话持久化机制,生产级推荐Clickhouse。\n\n- **Hitl(Human-in-the-Loop):**人工介入功能允许在智能体执行过程中,当遇到需要人工决策的关键节点时,暂停智能体执行并等待人工介入处理,使用参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-hitl\n\n- **智能体任务调度:灵活的智能体工作流任务**定时调度执行能力,可制定节假日或或者特定时间段忽略执行策略\n\n- **全链路监控:**智能体全链路 Trace 可观测性,覆盖 LLM 调用、工具执行、工作流编排;可基于智能体trace api,应用可自主记录智能体流程执行过程中的任何状态信息、数据信息和中间结果信息。\n\n### 支持的平台\n\n- DeepSeek\n- Kimi(Moonshot)\n- 智谱 AI(Zhipu)\n- 阿里百炼/通义千问(Qwen)\n- 字节豆包/火山引擎(Doubao)\n- 百度(Baidu)\n- 硅基流动(Siliconflow)\n- 九天(Jiutian)\n- MiniMax\n- 腾讯混元(TencentHY)\n- Xinference\n- OpenAI 兼容接口\n- ChatGPT\n\n## 二、环境准备\n\n### 2.1 Maven 依赖\n\n```xml\n<dependency>\n <groupId>com.bbossgroups</groupId>\n <artifactId>bboss-ai-flow</artifactId>\n <version>6.5.5</version>\n</dependency>\n```\n\n### 2.2 Gradle 依赖\n\n```groovy\nimplementation 'com.bbossgroups:bboss-ai-flow:6.5.5'\n```\n\n### 2.3 maas服务配置\n\n创建配置文件 `application-stream.properties`,配置各模型服务:使用时,需将apiKey替换为实际apiKey\n\n```properties\nhttp.poolNames = deepseek,qwenvlplus,volcengine,kimi,zhipu,minimax,hunyuan\n\n## DeepSeek 配置\ndeepseek.http.maxTotal = 200\ndeepseek.http.defaultMaxPerRoute = 200\ndeepseek.http.hosts=https://api.deepseek.com\ndeepseek.http.apiKeyId = sk-xxxxx\ndeepseek.http.modelType = deepseek\n\n## 阿里百炼通义千问配置\nqwenvlplus.http.maxTotal = 200\nqwenvlplus.http.defaultMaxPerRoute = 200\nqwenvlplus.http.hosts=https://dashscope.aliyuncs.com\nqwenvlplus.http.apiKeyId = sk-xxxxx\nqwenvlplus.http.modelType = qwen\n\n## 火山引擎豆包配置\nvolcengine.http.maxTotal = 200\nvolcengine.http.defaultMaxPerRoute = 200\nvolcengine.http.hosts=https://ark.cn-beijing.volces.com\nvolcengine.http.apiKeyId = xxxxx\nvolcengine.http.modelType = doubao\n\n## Kimi 配置\nkimi.http.maxTotal = 200\nkimi.http.defaultMaxPerRoute = 200\nkimi.http.hosts=https://api.moonshot.cn\nkimi.http.apiKeyId = sk-xxxxx\nkimi.http.modelType = kimi\n\n## 智谱配置\nzhipu.http.maxTotal = 200\nzhipu.http.defaultMaxPerRoute = 200\nzhipu.http.hosts=https://open.bigmodel.cn\nzhipu.http.apiKeyId = xxxxx\n\n## MiniMax 配置\nminimax.http.maxTotal = 200\nminimax.http.defaultMaxPerRoute = 200\nminimax.http.hosts=https://api.minimaxi.com\nminimax.http.apiKeyId = sk-xxxxx\nminimax.http.modelType = minimax\n\n## 腾讯混元配置\nhunyuan.http.maxTotal = 200\nhunyuan.http.defaultMaxPerRoute = 200\nhunyuan.http.hosts=https://api.hunyuan.cloud.tencent.com\nhunyuan.http.apiKeyId = sk-xxxxx\n```\n\n### 2.4 MCP 服务配置\n\n如需使用 MCP 工具服务,配置 `mcpserver.properties`:使用时,需将apiKey替换为实际apiKey\n\n```properties\nhttp.poolNames = aliyun,gaotie,amap,WebParser,visualops,12306,shuqi,feishumcp\n#分析文档,编写一篇简洁实用的bboss ai实用教程\n##aliyun mcp模型服务配置:在代码中引用服务的名称为aliyun\n# 服务连接池参数\naliyun.http.maxTotal = 200\naliyun.http.defaultMaxPerRoute = 200\n# aliyun模型服务地址\naliyun.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\naliyun.http.apiKeyId = sk-469f01dbb572310d6d6db4fb2f4\n# sse端点:高德地图用户key申请地址,https://console.amap.com/dev/key/app\naliyun.http.extendConfigs.sseendpoint = /api/v1/mcps/amap-maps/sse?key=793b8ba3f45a22fa74cba764a8\n\n##WebParser mcp模型服务配置:在代码中引用服务的名称为WebParser\n# 服务连接池参数\nWebParser.http.maxTotal = 200\nWebParser.http.defaultMaxPerRoute = 200\n# WebParser模型服务地址\nWebParser.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\nWebParser.http.apiKeyId = sk-469f01e6db310d6d6db4fb2f4\n# sse端点:高德地图用户key申请地址,https://console.amap.com/dev/key/app\nWebParser.http.extendConfigs.sseendpoint = /api/v1/mcps/WebParser/sse\n\n\n##gaode mcp模型服务配置:在代码中引用服务的名称为amap\n# 服务连接池参数\namap.http.maxTotal = 200\namap.http.defaultMaxPerRoute = 200\n# amap模型服务地址\namap.http.hosts=https://mcp.amap.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\n#amap.http.apiKeyId = sk-469f6db310d6d6db4fb2f4\n# sse端点:高德地图用户key申请地址,https://console.amap.com/dev/key/app\namap.http.extendConfigs.sseendpoint = /sse?key=793b8b9bab15a22fa74cba764a8\n\n\n##gaotie mcp模型服务配置:在代码中引用服务的名称为gaotie\n# 服务连接池参数\ngaotie.http.maxTotal = 200\ngaotie.http.defaultMaxPerRoute = 200\n# gaotie模型服务地址\ngaotie.http.hosts=127.0.0.1:8000\ngaotie.http.extendConfigs.sseendpoint = /sse\n\n##12306 mcp模型服务配置:在代码中引用服务的名称为12306\n# 服务连接池参数\n12306.http.maxTotal = 200\n12306.http.defaultMaxPerRoute = 200\n# 12306服务地址\n12306.http.hosts=https://dashscope.aliyuncs.com\n12306.http.apiKeyId = sk-469f01db6db310d6d6db4fb2f4\n12306.http.extendConfigs.sseendpoint = /api/v1/mcps/china-railway/sse\n\n##shuqi mcp模型服务配置:在代码中引用服务的名称为shuqi\n# 服务连接池参数\nshuqi.http.maxTotal = 200\nshuqi.http.defaultMaxPerRoute = 200\n# shuqi服务地址\nshuqi.http.hosts=https://dashscope.aliyuncs.com\nshuqi.http.apiKeyId = sk-469f01dbe6db310d6d6db4fb2f4\nshuqi.http.extendConfigs.streamableendpoint = /api/v1/mcps/market-cmapi00072981/mcp\n#shuqi.http.httpResponseInterceptors=org.frameworkset.spi.ai.HttpResponseInterceptorDemo\n\n##visualops mcp模型服务配置:在代码中引用服务的名称为visualops\n# 服务连接池参数\nvisualops.http.maxTotal = 200\nvisualops.http.defaultMaxPerRoute = 200\n# visualops服务地址\n\nvisualops.http.hosts=127.0.0.1:8080\nvisualops.http.apiKeyId = 17689048891086XsDsJVgwiQcmKhOdh23DX4NT\n#visualops.http.extendConfigs.sseendpoint = /mcp/sse.api\nvisualops.http.extendConfigs.streamableendpoint = /mcp/streamable.api\n\n##feishumcp mcp模型服务配置:在代码中引用服务的名称为feishumcp\n# 服务连接池参数\nfeishumcp.http.maxTotal = 200\nfeishumcp.http.defaultMaxPerRoute = 200\n# feishumcp服务地址\nfeishumcp.http.hosts=https://mcp.feishu.cn\nfeishumcp.http.extendConfigs.streamableendpoint = /mcp\n\n\n\n```\n\n---\n\n## 三、基础使用\n\n### 3.1 智能体消息类型\n\n系统内置消息类型定义:[SessionMessage](https://gitee.com/bboss/bboss-ai/blob/main/bboss-ai/src/main/java/org/frameworkset/spi/ai/store/SessionMessage.java)\n\n完整的消息类型对照表,可以参考:\n\n[16.2 消息类型对照表](#chapter16-2)\n\n#### 3.1.1 编码定义\n\n消息类型messageType,对应agent_session_message表中的messageType字段\n\n```java\n /**\n * 智能体输出消息:ASSISTANT中的一种\n */\n public static final String MESSAGE_TYPE_AGENT_RESULTMESSAGE = \"1\";\n /**\n * 智能体用户输入消息:提交给大模型或者其他多模态模型\n */\n public static final String MESSAGE_TYPE_USER_MESSAGE = \"2\";\n\n /**\n * 智能体辅助消息\n */\n public static final String MESSAGE_TYPE_ASSISTANT_MESSAGE = \"0\";\n\n\n /**\n * 智能体系统消息\n */\n public static final String MESSAGE_TYPE_SYSTEM_MESSAGE = \"3\";\n\n /**\n * 智能体辅助消息:工具调用结果消息,需要提交给大模型\n */\n public static final String MESSAGE_TYPE_TOOL_MESSAGE = \"4\";\n\n /**\n * 智能体跟踪消息\n */\n public static final String MESSAGE_TYPE_TRACE_MESSAGE = \"5\";\n\n /**\n * 智能体RAG知识消息\n */\n public static final String MESSAGE_TYPE_RAG_MESSAGE = \"6\";\n\n /**\n * 智能体拒答消息\n */\n public static final String MESSAGE_TYPE_REFUSE_MESSAGE = \"7\";\n /**\n * 智能体用户输入消息:包括用户输入的原始问题、用户上传文件、用户图片描述等\n */\n public static final String MESSAGE_TYPE_USER_INPUTMESSAGE = \"8\";\n\n /**\n * LLM输入消息\n */\n public static final String MESSAGE_TYPE_LLM_INPUTMESSAGE = \"9\";\n\n /**\n * LLM输出消息\n */\n public static final String MESSAGE_TYPE_LLM_OUTPUTMESSAGE = \"10\";\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_INPUTMESSAGE = \"11\";\n\n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_INPUTMESSAGE = \"12\";\n\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_OUTPUTMESSAGE = \"13\";\n \n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_OUTPUTMESSAGE = \"14\";\n /**\n * 智能体工具搜索匹配消息\n */\n public static final String MESSAGE_TYPE_TOOLSEARCH_MESSAGE = \"15\";\n\n\n /**\n * 智能体mcp服务调用消息\n */\n public static final String MESSAGE_TYPE_MCPCALL_MESSAGE = \"16\";\n\n /**\n * 智能体工具服务调用消息\n */\n public static final String MESSAGE_TYPE_TOOLCALL_MESSAGE = \"17\";\n\n\n /**\n * 智能体工具服务调用消息:其他消息类型,定义的消息名称(role)没有设置对应的编码,返回18\n * 需要在AgentMessageTypeConvertor中建立映射关系\n */\n public static final String MESSAGE_TYPE_OTHER_MESSAGE = \"18\";\n\t\n\t/**\n\t * 智能体Skill调用消息\n\t */\n\tpublic static final String MESSAGE_TYPE_SKILLCALL_MESSAGE = \"19\";\n\t\n\t/**\n\t * 智能体人工介入消息:大模型提供输入内容,需要人工介入处理\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_MESSAGE = \"20\";\n\t\n\t/**\n\t * 智能体人工介入处理消息:人工介入处理完成后,返回给大模型的消息和数据内容\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_HANDLE_MESSAGE = \"21\";\n\n\n```\n\n#### 3.1.2 名称定义\n\n消息类型messageType对应的角色名称:\n\n```java\n\n /**\n * 智能体用户输入消息:包括用户输入的原始问题、用户上传文件、用户图片描述等\n */\n public static final String MESSAGE_TYPE_USER_INPUTMESSAGE_NAME = \"userinput\";\n\n /**\n * LLM输入消息\n */\n public static final String MESSAGE_TYPE_LLM_INPUTMESSAGE_NAME = \"llminput\";\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_INPUTMESSAGE_NAME = \"embeddinginput\";\n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_INPUTMESSAGE_NAME = \"rerankinput\";\n\n\n /**\n * 向量模型输入消息\n */\n public static final String MESSAGE_TYPE_EMBEDDING_OUTPUTMESSAGE_NAME = \"embeddingoutput\";\n /**\n * RERANK模型输入消息\n */\n public static final String MESSAGE_TYPE_RERANK_OUTPUTMESSAGE_NAME = \"rerankoutput\";\n \n /**\n * LLM输出消息\n */\n public static final String MESSAGE_TYPE_LLM_OUTPUTMESSAGE_NAME = \"llmoutput\";\n /**\n * 智能体输出消息:ASSISTANT中的一种\n */\n public static final String MESSAGE_TYPE_AGENT_RESULTMESSAGE_NAME = \"agentresult\";\n /**\n * 智能体用户输入消息\n */\n public static final String MESSAGE_TYPE_USER_MESSAGE_NAME = \"user\";\n\n /**\n * 智能体辅助消息\n */\n public static final String MESSAGE_TYPE_ASSISTANT_MESSAGE_NAME = \"assistant\";\n\n /**\n * 智能体辅助消息:工具调用结果消息,需要提交给大模型\n */\n public static final String MESSAGE_TYPE_TOOL_MESSAGE_NAME = \"tool\";\n\n\n /**\n * 智能体工具搜索匹配消息\n */\n public static final String MESSAGE_TYPE_TOOLSEARCH_MESSAGE_NAME = \"toolsearch\";\n\n\n /**\n * 智能体系统消息\n */\n public static final String MESSAGE_TYPE_SYSTEM_MESSAGE_NAME = \"system\";\n\n\n\n /**\n * 智能体跟踪消息\n */\n public static final String MESSAGE_TYPE_TRACE_MESSAGE_NAME = \"trace\";\n\n /**\n * 智能体RAG知识消息\n */\n public static final String MESSAGE_TYPE_RAG_MESSAGE_NAME = \"rag\";\n\n /**\n * 智能体拒答消息\n */\n public static final String MESSAGE_TYPE_REFUSE_MESSAGE_NAME = \"refuse\";\n\n /**\n * 智能体mcp服务调用消息名称\n */\n public static final String MESSAGE_TYPE_MCPCALL_MESSAGE_NAME = \"mcpcall\";\n\n /**\n * 智能体工具服务调用消息\n */\n public static final String MESSAGE_TYPE_TOOLCALL_MESSAGE_NAME = \"toolcall\";\n\t\n\t/**\n\t * 智能体Skill调用消息\n\t */\n\tpublic static final String MESSAGE_TYPE_SKILLCALL_MESSAGE_NAME = \"skillcall\";\n\t\n\t/**\n\t * 智能体人工介入消息\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_MESSAGE_NAME = \"hitltask\";\n\t\n\t\n\t/**\n\t * 智能体人工介入处理消息\n\t */\n\tpublic static final String MESSAGE_TYPE_HITL_HANDLE_MESSAGE_NAME = \"hitlhandle\";\n```\n\n#### 3.1.3 扩展消息类型\n\nAgentMessageTypeConvertor提供以上内置角色名称到消息类型的转换,如果用户需要扩展自己的消息角色和编码,可以继承AgentMessageTypeConvertor类并重写convertMessageType方法,0-100为系统内置编码,如果用户自定义编码请从101开始,示例如下:\n\n```java\npublic class CustomAgentMessageTypeConvertor extends AgentMessageTypeConvertor {\n /**\n * 将角色转换为消息类型messageType,对应agent_session_message表中的messageType字段 \n *\n * @param role\n * @return\n */\n @Override\n public String convertMessageType(String role) {\n if(\"custom\".equals(role)){\n return \"101\";\n }\n return super.convertMessageType(role);\n }\n}\n```\n\n使用自定义转换器:\n\n```java\nStoreContext storeContext = new StoreContext();\nstoreContext.setAgentMessageTypeConvertor(new CustomAgentMessageTypeConvertor());\n```\n\n#### 3.1.3 使用场景\n\n消息类型使用场景:在智能体编排流程中设置消息角色类型\n\n```java\n TraceMessage traceMessage = new TraceMessage(); \n //记录用户输入的原始问题\n traceMessage.setMessage(Map.of(\"question\", question,\"role\",SessionMessage.MESSAGE_TYPE_USERINPUTMESSAGE_NAME));\n //其他用户上传的附件材料信息可以放到metaData中,也可以直接放到上面的消息中\n\n traceMessage.setStartTime(System.currentTimeMillis());\n\n planAgent.recordTraceMessage(traceMessage);\n```\n\n#### 3.1.4 ServerEvent结构\n\n```java\ncontentType:主要用来区分答案正文内容和思维链内容,0表示答案内容,1表示思维链内容, 2 表示工具调用,3 表示mcp服务调用,5 表示监控对象,默认值为0\n\n type 字段:标记消息报文类型 \n /**\n * 数据报文类型:0 数据消息,1表示异常消息,2 trace信息,traceId 3 拒答消息 5 知识库资料消息 6 步骤消息\n * 默认值为0 \n */\ntype = TYPE_DATA;\n数据报文类型常量说明\n /**\n * type:数据消息\n */\n public static final int TYPE_DATA = 0;\n /**\n * type:异常消息\n */\n public static final int TYPE_ERROR = 1;\n\n /**\n * type:trace信息,traceId\n */\n public static final int TYPE_TRACE = 2;\n /**\n * type:拒绝消息:\n */\n public static final int TYPE_REFUSAL = 3;\n \n /**\n * type:知识库资料消息:\n */\n public static final int TYPE_RAG_KNOWLEDGE = 5;\n\n /**\n * type:步骤消息:\n */\n public static final int TYPE_STEP = 6;\n```\n\n#### 3.1.5 属性说明\n\n| 参数 | 说明 |\n| -------------- | ---------------- |\n| `agentId` | 智能体唯一标识 |\n| `agentName` | 智能体名称 |\n| `prompt` | 用户提示词 |\n| `systemPrompt` | 系统提示词 |\n| `model` | 模型名称 |\n| `maas` | MaaS平台服务名 |\n| `stream` | 是否开启流式响应 |\n| `thinking` | 是否开启思考过程 |\n\n---\n\n### \n\n### 3.2 初始化配置\n\n在应用启动时加载配置文件:\n\n```java\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\n\npublic class Application {\n public void init() {\n // 加载模型服务配置文件\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n \n // 加载 MCP 服务配置文件\n HttpRequestProxy.startHttpPools(\"mcpserver.properties\");\n }\n}\n```\n\n### 3.3 同步聊天(非流式)\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.ChatAgentMessage;\nimport org.frameworkset.spi.ai.model.ServerEvent;\n\npublic class ChatExample {\n public static void main(String[] args) {\n // 初始化配置\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n \n // 创建消息对象\n ChatAgentMessage chatAgentMessage = new ChatAgentMessage();\n chatAgentMessage.setModel(\"deepseek-chat\"); // 模型名称\n chatAgentMessage.setPrompt(\"介绍一下 Java Reactor 编程模式\");\n chatAgentMessage.setSystemPrompt(\"你是一个编程大师\");\n chatAgentMessage.setTemperature(0.7); // 温度参数\n chatAgentMessage.setMaxTokens(8192); // 最大输出 Token\n \n // 创建 AIAgent 并调用\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.chat(\"deepseek\", chatAgentMessage);\n \n // 输出结果\n System.out.println(response.getData());\n }\n}\n```\n\n### 3.4 流式聊天\n\n```java\nimport reactor.core.publisher.Flux;\nimport java.util.concurrent.CountDownLatch;\n\npublic class StreamChatExample {\n public static void main(String[] args) throws InterruptedException {\n // 初始化配置\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n // 创建消息对象,设置 stream=true\n ChatAgentMessage chatAgentMessage = new ChatAgentMessage();\n chatAgentMessage.setModel(\"deepseek-chat\");\n chatAgentMessage.setPrompt(\"介绍一下 Java Reactor 编程模式\");\n chatAgentMessage.setStream(true);\n chatAgentMessage.setTemperature(0.7);\n chatAgentMessage.setMaxTokens(8192);\n\n CountDownLatch countDownLatch = new CountDownLatch(1);\n\n AIAgent aiAgent = new AIAgent();\n Flux<ServerEvent> flux = aiAgent.streamChat(\"deepseek\", chatAgentMessage);\n\n flux.doOnSubscribe(subscription -> System.out.println(\"开始订阅流...\"))\n .doOnNext(chunk -> {\n if (chunk.getData() != null) {\n System.out.print(chunk.getData());\n }\n })\n .doOnComplete(() -> {\n System.out.println(\"\\n=== 流完成 ===\");\n countDownLatch.countDown();\n })\n .doOnError(error -> {\n error.printStackTrace();\n countDownLatch.countDown();\n })\n .subscribe();\n\n // 等待异步完成\n countDownLatch.await();\n }\n}\n```\n\n### 3.5 多轮会话\n\n```java\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\n\npublic class SessionChatExample {\n // 静态会话记忆列表\n static List<Map<String, Object>> sessionMemory = new ArrayList<>();\n\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n AIAgent aiAgent = new AIAgent();\n\n // 第一轮对话\n ChatAgentMessage message1 = new ChatAgentMessage();\n message1.setModel(\"deepseek-chat\");\n message1.setPrompt(\"我叫张三\");\n message1.setSessionMemory(sessionMemory); // 传入会话记忆\n message1.setSessionSize(50); // 保留最近50条消息\n\n ServerEvent response1 = aiAgent.chat(\"deepseek\", message1);\n System.out.println(\"AI: \" + response1.getData());\n\n // 第二轮对话,AI 会记住用户名字\n ChatAgentMessage message2 = new ChatAgentMessage();\n message2.setModel(\"deepseek-chat\");\n message2.setPrompt(\"我叫什么名字?\");\n message2.setSessionMemory(sessionMemory);\n\n ServerEvent response2 = aiAgent.chat(\"deepseek\", message2);\n System.out.println(\"AI: \" + response2.getData());\n\n // 重置会话\n sessionMemory.clear();\n }\n}\n```\n\n---\n\n多智能体协同编排,访问章节:[多智能体协同编排](https://esdoc.bbossgroups.com/#/bboss-ai?id=%e5%8d%81%e5%9b%9b%e3%80%81%e5%a4%9a%e6%99%ba%e8%83%bd%e4%bd%93%e7%bc%96%e6%8e%92%e5%b7%a5%e4%bd%9c%e6%b5%81)\n\n### 3.6 失败重试\n\n如果模型服务不是太稳定,则需要设置重试机制,通过以下参数设置重试机制:\n\n**retry** 设置重试次数,大于0时,当调用大模型失败时,会自动重试,否则不重试,默认值0\n\n**retryInterval** 每次重试时,需等待特定时间后再重试,单位毫秒,大于0时才进行等待,默认值:500毫秒\n\n```java\nChatAgentMessage chatAgentMessage = new ChatAgentMessage();\nchatAgentMessage.setModel(\"deepseek-v4-pro\")\n .setStream( true)\n .setRetry(3).setRetryInterval(1000L)\n .setMaas(\"deepseek\").setTemperature(0.3)\n .setPrompt(question);\n```\n\n### 3.7 与spring boot 集成\n\n本节介绍bboss ai与spring boot集成,实现智能体问答功能。\n\n参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-qa\n\n## 四、图片识别(视觉大模型)\n\n### 4.1 单张图片识别\n\n```java\nimport org.frameworkset.spi.ai.model.ImageVLAgentMessage;\nimport com.frameworkset.util.FileUtil;\n\npublic class ImageRecognitionExample {\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n ImageVLAgentMessage imageMsg = new ImageVLAgentMessage();\n imageMsg.setModel(\"qwen3-vl-plus\");\n imageMsg.setPrompt(\"介绍图片内容\");\n\n // 方式1:使用图片 URL\n imageMsg.addImageUrl(\"https://example.com/image.jpg\");\n\n // 方式2:使用 Base64 编码\n String base64 = FileUtil.getBase64Content(\"/path/to/image.jpg\");\n imageMsg.addImageUrl(base64);\n\n // 可选参数\n imageMsg.addParameter(\"enable_thinking\", true); // 开启思考过程\n imageMsg.addParameter(\"thinking_budget\", 81920); // 推理 Token 数\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.imageParser(\"qwenvlplus\", imageMsg);\n\n System.out.println(response.getData());\n }\n}\n```\n\n### 4.2 流式图片识别\n\n```java\npublic static void streamImageRecognition() throws InterruptedException {\n ImageVLAgentMessage imageMsg = new ImageVLAgentMessage();\n imageMsg.setModel(\"qwen3-vl-plus\");\n imageMsg.setPrompt(\"介绍图片内容\");\n imageMsg.addImageUrl(\"https://example.com/image.jpg\");\n imageMsg.setStream(true);\n\n CountDownLatch latch = new CountDownLatch(1);\n\n AIAgent aiAgent = new AIAgent();\n Flux<ServerEvent> flux = aiAgent.streamImageParser(\"qwenvlplus\", imageMsg);\n\n flux.doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n}\n```\n\n### 4.3 多张图片对比\n\n```java\npublic static void compareImages() {\n ImageVLAgentMessage imageMsg = new ImageVLAgentMessage();\n imageMsg.setModel(\"qwen3-vl-plus\");\n imageMsg.setPrompt(\"对比两张图片的相似度,以 JSON 格式返回结果\");\n\n imageMsg.addImageUrl(\"https://example.com/image1.jpg\");\n imageMsg.addImageUrl(\"https://example.com/image2.jpg\");\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.imageParser(\"qwenvlplus\", imageMsg);\n\n System.out.println(response.getData());\n}\n```\n\n---\n\n## 五、图片生成\n\n### 5.1 文本生成图片\n\n```java\nimport org.frameworkset.spi.ai.model.ImageAgentMessage;\nimport org.frameworkset.spi.ai.model.ImageEvent;\n\npublic class ImageGenerationExample {\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n ImageAgentMessage request = new ImageAgentMessage();\n request.setPrompt(\"一只可爱的橘猫,坐在窗台上晒太阳,油画风格\");\n request.setModel(\"qwen-image-plus\");\n\n // 可选参数\n request.addParameter(\"size\", \"1328*1328\"); // 图片尺寸\n request.addParameter(\"watermark\", false); // 是否添加水印\n request.addParameter(\"prompt_extend\", true); // 提示词扩展\n\n // 设置图片保存路径\n request.setStoreFilePathFunction(imageUrl -> {\n return \"image/\" + System.currentTimeMillis() + \".jpg\";\n });\n\n AIAgent aiAgent = new AIAgent();\n ImageEvent result = aiAgent.genImage(\"qwenvlplus\", request);\n\n System.out.println(\"生成的图片 URL: \" + result.getGenImageUrl());\n System.out.println(\"本地保存路径: \" + result.getImageUrl());\n }\n}\n```\n\n### 5.2 火山引擎豆包图片生成\n\n```java\npublic static void doubaoImageGen() {\n ImageAgentMessage request = new ImageAgentMessage();\n request.setPrompt(\"一只可爱的小狗,卡通风格\");\n request.setModel(\"doubao-seedream-5-0-260128\");\n\n // 豆包特有参数\n request.addParameter(\"response_format\", \"url\");\n request.addParameter(\"size\", \"2k\");\n request.addParameter(\"watermark\", true);\n request.addParameter(\"sequential_image_generation\", \"auto\"); // 多图生成\n request.addMapParameter(\"sequential_image_generation_options\",\n \"max_images\", 3);\n\n AIAgent aiAgent = new AIAgent();\n ImageEvent result = aiAgent.genImage(\"volcengine\", request);\n\n System.out.println(\"生成图片数量: \" + result.getImageUrls().size());\n}\n```\n\n### 5.3 图文生图(修图)\n\n```java\nString selectedModel = (String)questions.get(\"selectedModel\");\nboolean generateMultipleImages = questions != null?(boolean)questions.get(\"generateMultipleImages\"):false;\nString message = null;\nmessage = questions != null?(String)questions.get(\"message\"):null;\n if(SimpleStringUtil.isEmpty( message)){\nmessage = \"生成一颗桂花树\";\n }\nImageAgentMessage request = new ImageAgentMessage();\n request.setStoreFilePathFunction(new StoreFilePathFunction() {\n @Override\n public String getStoreFilePath(String imageUrl) {\n return \"image/\"+SimpleStringUtil.getUUID32() +\".jpg\";\n }\n});\n\nList<String> imagesBase64 = (List)questions.get(\"imagesBase64\");\nString imageUrl = (String)questions.get(\"imageUrl\");\n if(imageUrl != null) {\nimageUrl = imageUrl.trim();\n }\n\n\n\n\n if(SimpleStringUtil.isNotEmpty(imageUrl)) {\n request.addImageUrl(imageUrl);\n }\n if(SimpleStringUtil.isNotEmpty(imagesBase64)) {\n for(String tmp:imagesBase64) {\n request.addImageUrl(tmp);\n// Map<String, Object> requestMap = new HashMap<>();\n// requestMap.put(\"model\", \"LLMImage2Text\");\n// requestMap.put(\"image\",tmp);\n// requestMap.put(\"prompt\", message);\n// requestMap.put(\"stream\", true);\n// String rsp = HttpRequestProxy.httpPostforString(\"jiutian\", \"/largemodel/moma/api/v3/image/text\", requestMap);\n// logger.info(rsp);\n }\n\n\n }\n request.setPrompt( message);\nImageEvent data = null;\nAIAgent aiAgent = new AIAgent();\n// String completionsUrl = null;\n if(selectedModel.equals(\"volcengine\")){\n //字节火山引擎\n request.setModel( \"doubao-seedream-4-5-251128\");\n if(!generateMultipleImages) {\n request.addParameter(\"sequential_image_generation\", \"disabled\");//生成单图\n }\n else {\n request.addParameter(\"sequential_image_generation\", \"auto\");//生成多图\n request.addMapParameter(\"sequential_image_generation_options\",\"max_images\",5);\n }\n request.addParameter(\"response_format\", \"url\");\n request.addParameter(\"size\", \"2k\");\n request.addParameter(\"watermark\", true);\n// completionsUrl = \"/api/v3/images/generations\";\n }\n else if(selectedModel.equals(\"jiutian\")){\n //字节火山引擎\n request.setModel( \"cntxt2image\");\n// completionsUrl = \"/largemodel/moma/api/v3/images/generations\";\n }\n else{\n\n //阿里百炼\n //通过在http.modelType指定全局模型适配器类型,亦可以在ImageAgentMessage对象设置请求级别modelType模型适配器类型(优先级高于全局模型适配器类型)\n request.setModel( \"qwen-image-edit-max-2026-01-16\");\n request.addParameter(\"n\",5);\n request.addParameter(\"prompt_extend\",true);\n request.addParameter(\"watermark\",false);\n request.addParameter(\"size\",\"1536*1024\");\n///api/v1/services/aigc/multimodal-generation/generation\n// completionsUrl = \"/api/v1/services/aigc/multimodal-generation/generation\";\n\n }\nImageEvent imagedata = aiAgent.genImage(selectedModel,request);\nSystem.out.println(\"生成图片数量: \" + result.getImageUrls().size());\n```\n\n---\n\n## 六、语音识别(STT)\n\n```java\nimport org.frameworkset.spi.ai.model.AudioSTTAgentMessage;\n\npublic class SpeechRecognitionExample {\n public static void main(String[] args) throws IOException {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n AudioSTTAgentMessage audioMsg = new AudioSTTAgentMessage();\n audioMsg.setModel(\"qwen3-asr-flash\");\n audioMsg.setPrompt(\"介绍音频内容\");\n\n // 方式1:设置音频文件\n File audioFile = new File(\"/path/to/audio.wav\");\n audioMsg.setAudio(audioFile);\n audioMsg.setContentType(\"audio/wav\");\n\n // 方式2:设置音频 URL\n // audioMsg.setAudio(\"https://example.com/audio.mp3\");\n\n // 可选参数\n audioMsg.addMapParameter(\"asr_options\", \"enable_itn\", true);\n audioMsg.addParameter(\"incremental_output\", true);\n audioMsg.setResultFormat(\"message\");\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent result = aiAgent.audioParser(\"qwenvlplus\", audioMsg);\n\n System.out.println(\"识别结果: \" + result.getData());\n }\n}\n```\n\n---\n\n## 七、语音合成(TTS)\n\n### 7.1 同步语音合成\n\n通义千问语音生成:\n\n```java\nimport org.frameworkset.spi.ai.model.AudioAgentMessage;\nimport org.frameworkset.spi.ai.model.AudioEvent;\n\npublic class TextToSpeechExample {\n public static void main(String[] args) {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n\n AudioAgentMessage audioMsg = new AudioAgentMessage();\n audioMsg.setModel(\"qwen3-tts-flash\");\n audioMsg.setPrompt(\"诗歌朗诵:床前明月光,疑似地上霜。\");\n\n // 可选参数\n audioMsg.addParameter(\"voice\", \"Cherry\"); // 音色\n audioMsg.addParameter(\"language_type\", \"Chinese\"); // 语言\n\n // 设置保存路径,通过StoreFilePathFunction保存音频文件到本地\n audioMsg.setStoreFilePathFunction(url -> {\n return \"audio/\" + System.currentTimeMillis() + \".wav\";\n });\n\n AIAgent aiAgent = new AIAgent();\n AudioEvent result = aiAgent.genAudio(\"qwenvlplus\", audioMsg);\n\n System.out.println(\"音频文件路径: \" + result.getAudioUrl());\n }\n}\n```\n\n智谱语音生成:\n\n```java\n AudioAgentMessage audioMsg = new AudioAgentMessage();\n //https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E6%96%87%E6%9C%AC%E8%BD%AC%E8%AF%AD%E9%9F%B3\n \n \t\t\taudioMsg.setModel(\"glm-tts\");\n \taudioMsg.setPrompt(\"诗歌朗诵:床前明月光,疑似地上霜。\");\n audioAgentMessage.addParameter(\"voice\", \"female\")\n .addParameter(\"response_format\", \"wav\")\n .addParameter(\"speed\", 1.0)\n .addParameter(\"volume\", 1.0) \n ;\n // 设置保存路径,智谱语音模型默认返回对应音频文件格式的base64字符串。\n audioMsg.setStoreFilePathFunction(new ReponseStoreFilePathFunction() {\n @Override\n public String getStoreFilePath(String imageUrl) {\n return \"audio/\"+SimpleStringUtil.getUUID32() +\".wav\";\n }\n }); \n \n \n AIAgent aiAgent = new AIAgent();\n AudioEvent audioEvent = aiAgent.genAudio(selectedModel,audioAgentMessage,storeFilePathFunction);\n return audioEvent;\n```\n\nStoreFilePathFunction和ReponseStoreFilePathFunction区别:\n\nStoreFilePathFunction:当模型将生成的语音通过url方式返回时,采用StoreFilePathFunction保存到本地音频文件\n\nReponseStoreFilePathFunction:当模型将生成的语音通过base64码方式返回时,采用ReponseStoreFilePathFunction直接将base64编码音频内容保存到本地音频文件\n\n### 7.2 流式语音合成\n\n```java\npublic static void streamTextToSpeech() throws InterruptedException {\n AudioAgentMessage audioMsg = new AudioAgentMessage();\n audioMsg.setModel(\"qwen3-tts-flash\");\n audioMsg.setPrompt(\"欢迎使用 bboss AI 语音合成服务\");\n audioMsg.addParameter(\"voice\", \"Cherry\");\n audioMsg.setStream(true);\n\n CountDownLatch latch = new CountDownLatch(1);\n\n AIAgent aiAgent = new AIAgent();\n Flux<ServerEvent> flux = aiAgent.streamAudioGen(\"qwenvlplus\", audioMsg);\n\n flux.doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n // 处理音频数据块\n byte[] audioData = Base64.getDecoder().decode(chunk.getData());\n // 播放或保存音频数据\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n}\n```\n\n---\n\n## 八、工具调用(Function Calling)\n\n基于bboss发布工具函数参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-toolannotation\n\n高效工具检索功能参考使用文档:https://esdoc.bbossgroups.com/#/bboss-ai-toolsearcher\n\n### 8.1 本地工具调用\n\n```java\nimport org.frameworkset.spi.ai.model.FunctionToolDefine;\nimport org.frameworkset.spi.ai.model.ToolFunctionCall;\n\npublic class ToolCallExample {\n public static void main(String[] args) {\n ChatAgentMessage chatMsg = new ChatAgentMessage();\n chatMsg.setModel(\"deepseek-chat\");\n chatMsg.setPrompt(\"查询杭州的天气,并给出穿衣建议\");\n\n // 定义天气查询工具\n FunctionToolDefine weatherTool = new FunctionToolDefine();\n weatherTool.funtionName2ndDescription(\"weather_info_query\",\"天气查询服务,根据城市查询当地温度和天气信息\")\n// .putParametersType(\"object\")\n .requiredParameters(\"location\")\n .addSubParameter(\"params\",\"location\",\"string\",\"城市或者地州, 例如:上海市\")\n .setFunctionCall(new ToolFunctionCall() );\n\n chatMsg.registTool(weatherTool);\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.chat(\"deepseek\", chatMsg);\n System.out.println(response.getData());\n }\n\n\n}\n```\n\nToolFunctionCall工具调用实现:\n\n```java\nimport org.frameworkset.spi.ai.model.FunctionCall;\nimport org.frameworkset.spi.ai.model.FunctionCallException;\nimport org.frameworkset.spi.ai.model.FunctionTool;\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\n\nimport java.util.List;\nimport java.util.Map;\n\n\npublic class ToolFunctionCall implements FunctionCall {\n private String toolDatasource = \"tool\";\n\n public ToolFunctionCall(String toolDatasource) {\n this.toolDatasource = toolDatasource;\n }\n public ToolFunctionCall( ) {\n }\n\n\n @Override\n public Object call(FunctionTool functionTool) throws FunctionCallException {\n Map response = HttpRequestProxy.sendJsonBody(toolDatasource,functionTool.getArguments(), \"/openapi/\"+functionTool.getFunctionName() + \".api\", Map.class);\n List data = (List)response.get(\"data\");\n if(data == null || data.size() == 0){\n return \"没有找到对应数据\";\n }\n return data;\n// return \"20.22℃\";\n }\n}\n```\n\n\n\n### 8.2 远程工具调用\n\n```java\nchatMsg.setToolsRegist(new ToolsRegist() {\n @Override\n public List<FunctionToolDefine> registTools() {\n // 从远程服务获取工具定义\n Map<String, Object> params = new HashMap<>();\n params.put(\"apiKey\", \"your-api-key\");\n return HttpRequestProxy.sendJsonBodyForList(\"tool\", params,\n \"/function/tools.api\", FunctionToolDefine.class);\n }\n\n @Override\n public FunctionCall getFunctionCall(String functionName) {\n // 返回远程工具调用器\n return new ToolFunctionCall(\"tool\");\n }\n});\n```\n\n---\n\n### 8.3 多轮工具调用(Loop Tool Call)\n\n对于需要多步骤执行的复杂任务,智能体可能需要**多次调用工具**才能完成。bboss-ai 内置了循环工具调用机制,允许模型根据前序工具执行结果,自动决策是否继续调用下一个工具。\n\n**核心 API:**\n\n| 方法 | 说明 |\n| ---------------------------------------- | ----------------------------------- |\n| `AIAgent.setEnableLoopToolCall(boolean)` | 启用/禁用循环工具调用,默认 `false` |\n| `AIAgent.setMaxLoopToolCalls(int)` | 设置最大循环次数,防止无限循环 |\n\n使用示例:\n\n1)提示词资源文件中定义多轮任务\n\n```markdown\n用户要求:#[input.query],请依次执行以下命令:\n1.获取OS版本信息\n2.获取CPU信息\n3.打印OS和CPU信息\n4.查找占用808端口的进程,如果存在对应进程,则关闭进程。注意:端口是808,不要查找其他端口\n6.打印端口进程信息和端口关闭结果\n```\n2)智能体执行多轮任务\n\n1. 加载多任务提示词工程\n2. 启用智能体多次调用工具机制(默认关闭)\n3. 注册智能体完成任务所需的工具清单\n```java\n AINodeAgent scan2ndClosePortProcessAgent = new AINodeAgent(\"#[prompts/rag-looptoolcall-prompt.txt,type=resource]\")\n .setSystemPrompt(\"你是一个专家,可以根据用户要求获取系统信息,生成符合要求的、完整的、可执行的shell脚本\" +\n \",并将生成的脚本交由工具执行,输出执行结果。注意事项:通过Java Process调用cmd或者sh来执行脚本,确保脚本在目标操作系统上能够正常运行。\")\n .setAgentId(\"scan2ndClosePortProcessAgent\").setAgentName(\"扫描并关闭端口进程\");\n scan2ndClosePortProcessAgent.setEnableLoopToolCall(true);//启用智能体多次调用工具机制\n scan2ndClosePortProcessAgent.setMaxLoopToolCalls(100);\n //注册获取当前操作系统OS信息工具:框架内置工具\n scan2ndClosePortProcessAgent.registBeanTool(new GetOSFunctionTool(60));\n// //注册脚本执行工具,会根据获取到的OS信息,生成对应的OS环境命令行脚本进行执行:框架内置工具\n scan2ndClosePortProcessAgent.registBeanTool(new CLIShellFunctionTool(60));\n```\n\n\n\n### 8.4 内置工具\n\nbboss提供了一系列内置工具,比如脚本执行、文件操作、获取系统信息、代码执行等,可以访问以下文档了解内置工具的介绍和使用方法:\n\nhttps://esdoc.bbossgroups.com/#/bboss-ai-innertools\n\n## 九、MCP 服务集成\n\n基于bboss发布mcp服务参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-mcpbean\n\n基于bboss发布mcp服务并与spring boot集成参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-springboot\n\n### 9.1 使用 MCP 工具\n\n```java\nimport org.frameworkset.spi.ai.mcp.tools.MCPToolsRegist;\n\npublic class MCPExample {\n public static void main(String[] args) {\n ChatAgentMessage chatMsg = new ChatAgentMessage();\n chatMsg.setModel(\"deepseek-chat\");\n chatMsg.setPrompt(\"帮我查一下明天北京到上海的高铁\");\n\n // 注册 12306高铁MCP 工具服务,\n chatMsg.setToolsRegist(new MCPToolsRegist(\"12306\")); // 对应配置文件中的服务名\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.chat(\"deepseek\", chatMsg);\n System.out.println(response.getData());\n }\n}\n```\n\n### 9.2 流式 MCP 调用\n\n```java\npublic static void streamChatWithMcpTools(String maas, String mcpServer,\n String model, String prompt)\n throws InterruptedException {\n\n List<Map<String, Object>> session = new ArrayList<>();\n ChatAgentMessage chatMsg = new ChatAgentMessage()\n .setPrompt(prompt)\n .setSessionSize(50)\n .setSessionMemory(session)\n .setModel(model)\n .setStream(true)\n .setMaxTokens(4096);\n\n chatMsg.setToolsRegist(new MCPToolsRegist(mcpServer));\n\n CountDownLatch latch = new CountDownLatch(1);\n AIAgent aiAgent = new AIAgent();\n\n aiAgent.streamChat(maas, chatMsg)\n .doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n } else if (chunk.isToolCallsType()) {\n System.out.println(\"\\n开始执行工具...\");\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n}\n```\n\n---\n\n### 9.3 飞书MCP服务集成调用\n\n飞书MCP服务实现与标准MCP服务有所差别:需要通过http header设置临时AccessToken和工具清单配置\n\n飞书开发者调用远程 MCP 服务文档:\n\nhttps://open.feishu.cn/document/mcp_open_tools/developers-call-remote-mcp-server\n\n下面是bboss对接和使用飞书Mcp服务的案例代码:feishumcp server配置,参考前文的mcp服务配置,基于streamable 访问飞书Mcp服务\n\n```java\n BaseFeishuConfig baseFeishuConfig = new BaseFeishuConfig();\n// 设置bboss应用id和应用token,用于申请访问飞书Mcp服务的AccessToken\n baseFeishuConfig.setFeishuAppId(\"cli_a9d43b8f89cd0\")\n .setFeishAppSecret(\"gIhy0EbVfgQGlpNBN8rqMKMnYCJs\");\n//定义用于获取飞书Mcp服务认证Token的http数据源:feishu,用例访问开发平台获取飞书MCP服务认证Token\n baseFeishuConfig.addHttpConfig(\"http.poolNames\", \"feishu\")\n .addHttpConfig(\"feishu.http.hosts\", \"https://open.feishu.cn\")\n .addHttpConfig(\"feishu.http.maxTotal\", 100)\n .addHttpConfig(\"feishu.http.defaultMaxPerRoute\", 100)\n .addHttpConfig(\"feishu.http.authorTokenFunction\", \"org.frameworkset.spi.ai.mcp.feishu.FeishuMCPAuthorTokenFunction\")\n feishumcp.http.authorTokenFunction = \n// 25分钟自动刷新token\n// tenant_access_token 的最大有效期是 2 小时。\n// 剩余有效期小于 30 分钟时,调用本接口会返回一个新的 tenant_access_token,这会同时存在两个有效的 tenant_access_token。\n// 剩余有效期大于等于 30 分钟时,调用本接口会返回原有的 tenant_access_token\nfeishumcp.http.authorTokenExpiredTime = 1500000\n \n .addHttpConfig(\"feishu.http.authorTokenExpiredTime\", 1500000)\n .setMcpTools(\"search-user,get-user,fetch-file,search-doc,create-doc,fetch-doc,update-doc,list-docs,get-comments,add-comments\");//需要设置飞书Mcp工具清单,否则大模型无法识别Mcp工具并调用\n;\n chatAgentMessage.setToolsRegist(new FeishuMcpRegist(\"feishumcp\",baseFeishuConfig));\n\nCountDownLatch latch = new CountDownLatch(1);\nAIAgent aiAgent = new AIAgent();\n\n aiAgent.streamChat(maas, chatMsg)\n .doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n } else if (chunk.isToolCallsType()) {\n System.out.println(\"\\n开始执行工具...\");\n }\n })\n .doOnComplete(() -> latch.countDown())\n .subscribe();\n\n latch.await();\n```\n\n### 9.4 MCP 客户端\n\n`MCPClient` 实现了 Model Context Protocol 协议客户端,支持两种传输模式:\n\n#### 9.4.1 **核心功能**\n\n- SSE(Server-Sent Events)连接管理(`MCPSSEClient`)\n- Streamable HTTP 连接管理(`MCPStreamableClient`)\n- 会话生命周期管理(initialize、notifications/initialized)\n- 工具列表获取(tools/list)\n- 工具调用(tools/call)\n- 飞书 MCP 集成(`FeishuMCPClient`、`FeishuMCPStreamableClient`)\n\n#### 9.4.2 **工作流程**\n\n1. 通过 SSE 端点或 Streamable HTTP 端点建立连接\n2. 接收 endpoint 事件获取 messagePath 和 sessionId\n3. 发送 initialize 请求进行协议初始化\n4. 发送 notifications/initialized 通知服务端\n5. 正常进行工具列表查询和调用\n\n#### **9.4.3 Spring Boot 客户端集成示例**\n\n```java\n// 1. 配置 mcpserver.properties\n// http.poolNames = mcp_server\n// mcp_server.http.hosts = 127.0.0.1:8889\n// mcp_server.http.apiKeyId = 123456\n// mcp_server.http.extendConfigs.streamableendpoint = /ecop-biz-srv/mcp/streamable\n\n// 2. 启动时初始化 MCP 连接池\n@Component\npublic class AgentBootrap {\n @PostConstruct\n public void start() {\n // 初始化大模型 maas 服务\n HttpRequestProxy.startHttpPools(\"maas.properties\");\n // 初始化 mcp 服务\n HttpRequestProxy.startHttpPools(\"mcpserver.properties\");\n }\n}\n\n// 3. Spring Boot 配置类注册 MCP 工具\n@Configuration\npublic class EcopAgentMcpClientFactory {\n @Bean(\"ecopConfigMcpServerToolsRegist\")\n public ToolsRegist buildEcopConfigMcpServerToolsRegist() {\n // MCPToolsRegist 通过服务名称关联到 mcpserver.properties 配置\n ToolsRegist toolsRegist = new MCPToolsRegist(\"mcp_server\");\n return toolsRegist;\n }\n}\n\n// 4. 在 AIAgent 中使用 MCP 工具\n@Service\npublic class RagQAService {\n @Autowired\n @Qualifier(\"ecopConfigMcpServerToolsRegist\")\n private ToolsRegist mcpToolsRegist;\n\n public ServerEvent chat(String question) {\n AIAgent agent = new AIAgent();\n agent.setToolsRegist(mcpToolsRegist);\n\n ChatAgentMessage message = new ChatAgentMessage();\n message.setPrompt(question);\n message.setModel(\"deepseek-chat\");\n\n return agent.chat(\"maasName\", message);\n }\n}\n```\n\n#### 9.4.4**配置说明**\n\n| 配置项 | 说明 |\n| -------------------------------------------------- | ---------------------------- |\n| `http.poolNames` | 连接池名称列表,对应服务标识 |\n| `{poolName}.http.hosts` | MCP 服务端地址 |\n| `{poolName}.http.apiKeyId` | 访问 MCP 服务端的 API 密钥 |\n| `{poolName}.http.extendConfigs.streamableendpoint` | Streamable HTTP 端点路径 |\n| `{poolName}.http.extendConfigs.sseendpoint` | SSE 端点路径(可选) |\n\n#### 9.4.5**MCPToolsRegist 工作原理**\n\n1. `MCPToolsRegist` 通过服务名称(如 `mcp_server`)关联到 `mcpserver.properties` 中的连接池配置\n\n2. 初始化时向 MCP 服务端发送 `initialize` 请求进行协议握手\n\n3. 调用 `tools/list` 获取服务端暴露的工具列表\n\n4. 将工具列表转换为 `FunctionToolDefine` 供 `AIAgent` 使用\n\n5. 当 AI 模型需要调用工具时,`MCPToolsRegist` 通过 `tools/call` 将请求转发到 MCP 服务端执行\n\n#### 9.4.6 Spring ai mcp服务兼容性设置\n\n采用streamable api协议对接Spring ai mcp服务时,需要额外设置一下http请求拦截器。\n\n编写一个简单的httprequest拦截器SpringAIMcpRequestIntercepter(内置在bboss框架包,无需自行编写,只需进行下一步的配置即可):\n\n```java\npackage org.frameworkset.spi.ai.mcp.intercepter;\n\nimport org.apache.hc.core5.http.EntityDetails;\nimport org.apache.hc.core5.http.HttpException;\nimport org.apache.hc.core5.http.HttpRequest;\nimport org.apache.hc.core5.http.protocol.HttpContext;\nimport org.frameworkset.spi.remote.http.callback.ClientConfigurationHttpRequestInterceptor;\n\nimport java.io.IOException;\n\n/**\n * spring ai mcp 服务兼容mcp请求拦截器\n * @author biaoping.yin\n * @Date 2026/7/31\n */\npublic class SpringAIMcpRequestIntercepter extends ClientConfigurationHttpRequestInterceptor {\n \n @Override\n public void process(HttpRequest httpRequest, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException {\n try {\n \n httpRequest.addHeader(\"Accept\", \"application/json, text/event-stream\");\n } catch (Exception e) {\n throw new RuntimeException(e);\n }\n \n }\n \n}\n```\n\n spring ai mcp 服务兼容mcp请求拦截器,在mcpserver配置文件进行如下配置即可:test为mcp数据源名称\n\n```properties\ntest.http.httpRequestInterceptors=org.frameworkset.spi.ai.mcp.intercepter.SpringAIMcpRequestIntercepter\n```\n\n\n\n### 9.5 MCP 服务端\n\nbboss-ai 不仅可以作为 MCP 客户端调用外部工具,还可以作为 MCP 服务端对外暴露工具能力:\n\n#### **9.5.1 核心组件**\n\n| 类名 | 作用 |\n| ------------------------------------------------ | ------------------------------------------------------------ |\n| `MCPToolService` / `MCPToolServiceImpl` | MCP 服务端接口实现,提供 SSE 和 Streamable HTTP 两种服务端接口 |\n| `MCPApiKeyService` / `MCPApiKeyServiceImpl` | API 密钥认证与工具注册管理 |\n| `MCPBeanToolsRegist` / `MCPBeanToolFunctionCall` | Bean 工具服务端注册与调用 |\n| `MCPApiRequestUtil` | 服务端请求响应构建工具 |\n\n#### 9.5.2 **服务端能力**\n\n- **SSE 模式**:`sse()` 建立 SSE 长连接,`message()` 处理客户端消息\n- **Streamable HTTP 模式**:`streamable()` 处理无状态请求\n- **API 密钥认证**:支持按 apiKey 注册和授权访问工具\n- **工具权限控制**:支持按 functionName + apiKey 细粒度授权\n- **支持的方法**:initialize、notifications/initialized、tools/list、tools/call\n\n#### 9.5.3 **服务端工作流程**\n\n1. 客户端通过 `registMcpBeanTool(apiKey, bean)` 注册 Bean 工具到指定密钥\n2. 客户端通过 SSE 端点建立连接或发送 Streamable HTTP 请求\n3. 服务端校验 apiKey 合法性\n4. 根据请求方法返回工具列表或执行工具调用\n5. 通过 SSE Sink 或 HTTP Response 返回结果\n\n#### 9.5.4 **Spring Boot 集成示例**\n\n```java\n// 1. 使用 @Tool 注解定义工具\n@Service\npublic class Hotel2ndFlightBookTool {\n @Tool(name=\"hotelQuery\", description=\"根据用户的行程需求,查询合适的酒店。\")\n public List<Map> hotelQuery(\n @ToolParam(name=\"startDay\", description=\"入驻时间,例如:5月25日\", required=true) String startDay,\n @ToolParam(name=\"endDay\", description=\"离房时间,例如:5月28日\", required=true) String endDay\n ) {\n // 业务逻辑实现\n return hotels;\n }\n\n @Tool(name=\"flightQuery\", description=\"根据用户的行程需求,查询合适的航班机票。\")\n public List<Map> flightQuery(\n @ToolParam(name=\"bookDay\", description=\"出发时间,例如:5月25日\", required=true) String bookDay,\n @ToolParam(name=\"arriveDay\", description=\"到达时间,例如:5月28日\", required=true) String arriveDay,\n @ToolParam(name=\"fromStation\", description=\"出发地,例如:长沙\", required=true) String fromStation,\n @ToolParam(name=\"toStation\", description=\"到达地,例如:北京\", required=true) String toStation\n ) {\n // 业务逻辑实现\n return flights;\n }\n}\n\n// 2. Spring Boot 配置类注册 MCP 服务\n@Configuration\npublic class EcopAgentMcpServiceFactory {\n @Autowired\n private Hotel2ndFlightBookTool hotel2ndFlightBookTool;\n\n @Bean(\"ecopConfigMCPToolService\")\n public MCPToolService buildEcopConfigMCPToolService() {\n MCPApiKeyServiceImpl mcpApiKeyService = new MCPApiKeyServiceImpl();\n // 将组件中定义的工具方法注册为 MCP 服务\n mcpApiKeyService.registMcpBeanTool(\"123456\", hotel2ndFlightBookTool);\n\n MCPToolServiceImpl mcpService = new MCPToolServiceImpl();\n mcpService.setMcpApiKeyService(mcpApiKeyService);\n return mcpService;\n }\n}\n\n// 3. Spring Boot Controller 暴露 MCP 端点\n@RestController\n@RequestMapping(\"/mcp\")\npublic class MCPServerController {\n @Autowired\n @Qualifier(\"ecopConfigMCPToolService\")\n private MCPToolService ecopConfigMCPToolService;\n\n // SSE 模式端点\n @GetMapping(\"/sse\")\n public Flux<String> sse(@RequestHeader(name=\"Authorization\") String authorizationHeader) {\n String apiKey = HttpRequestProxy.extractApiKeyFromBearer(authorizationHeader);\n return ecopConfigMCPToolService.sse(apiKey);\n }\n\n // SSE 消息处理端点\n @PostMapping(\"/message\")\n public String message(\n @RequestHeader(name=\"Authorization\") String authorizationHeader,\n @RequestParam(name = \"sessionId\") String sessionId,\n @RequestBody String requestBody\n ) {\n String apiKey = HttpRequestProxy.extractApiKeyFromBearer(authorizationHeader);\n return ecopConfigMCPToolService.message(apiKey, sessionId, requestBody);\n }\n\n // Streamable HTTP 模式端点\n @PostMapping(\"/streamable\")\n public Object streamable(\n @RequestHeader(name=\"Authorization\") String authorizationHeader,\n @RequestBody String requestBody\n ) {\n String apiKey = HttpRequestProxy.extractApiKeyFromBearer(authorizationHeader);\n return ecopConfigMCPToolService.streamable(apiKey, requestBody);\n }\n}\n```\n\n\n\n## 十、视频识别与生成\n\n### 10.1 视频内容识别\n\n```java\npublic static void videoRecognition() {\n VideoVLAgentMessage videoMsg = new VideoVLAgentMessage();\n videoMsg.setModel(\"kimi-k2.5\");\n videoMsg.setPrompt(\"识别视频内容\");\n\n // 使用 Base64 编码的视频文件\n String base64 = FileUtil.getBase64Video(\"/path/to/video.mp4\");\n videoMsg.addVideoUrl(base64);\n\n // 可选参数\n videoMsg.addMapParameter(\"thinking\", \"type\", \"disabled\");\n\n AIAgent aiAgent = new AIAgent();\n ServerEvent response = aiAgent.videoParser(\"kimi\", videoMsg);\n System.out.println(response.getData());\n}\n```\n\n### 10.2 提交视频生成任务\n\n```\npublic static void submitVideoTask() {\n VideoAgentMessage videoMsg = new VideoAgentMessage();\n videoMsg.setModel(\"wan2.6-t2v\");\n videoMsg.setPrompt(\"一只可爱的小猫在花园里玩耍\");\n \n // 可选参数\n videoMsg.addParameter(\"size\", \"1280*720\");\n videoMsg.addParameter(\"duration\", 10);\n videoMsg.addParameter(\"prompt_extend\", true);\n videoMsg.addParameter(\"watermark\", true);\n \n AIAgent aiAgent = new AIAgent();\n VideoTask task = aiAgent.submitVideoTask(\"qwenvlplus\", videoMsg);\n \n System.out.println(\"任务 ID: \" + task.getTaskId());\n System.out.println(\"任务状态: \" + task.getTaskStatus());\n}\n```\n\n### 10.3 查询视频生成结果\n\n```\npublic static void getVideoTaskResult(String taskId) {\n VideoStoreAgentMessage queryMsg = new VideoStoreAgentMessage();\n queryMsg.setTaskId(taskId);\n queryMsg.setStoreFilePathFunction(url -> {\n return \"video/\" + System.currentTimeMillis() + \".mp4\";\n });\n \n AIAgent aiAgent = new AIAgent();\n VideoGenResult result = aiAgent.getVideoTaskResult(\"qwenvlplus\", queryMsg);\n \n System.out.println(\"任务状态: \" + result.getTaskStatus());\n System.out.println(\"视频 URL: \" + result.getVideoGenUrl());\n System.out.println(\"本地保存路径: \" + result.getVideoUrl());\n}\n```\n\n---\n## 十一、私有Maas平台对接\n\nbboss默认做了对国内主流公有云maas平台的兼容和支持,包括DeepSeek、Kimi、智谱、阿里百炼通义千问、字节豆包火山引擎、MiniMax、腾讯混元、中国移动九天等主流 MaaS 平台。\n\n如果需要对接遵循openai规范的本地私有Maas平台,只需编写自定义的适配器,并进行注册即可,本节介绍私有Maas平台对接方法。\n\n### 11.1 编写自定义适配器\n\n首先,编写自定义适配器,示例如下:直接继承已有通义千问适配器QwenAgentAdapter或者其他现有适配器,重载各种多模态能力相对地址获取方法即可\n\n```java\npackage org.frameworkset.spi.ai;\n\nimport org.frameworkset.spi.ai.adapter.QwenAgentAdapter;\nimport org.frameworkset.spi.ai.model.*;\n\n\npublic class CustomAgentAdapter extends QwenAgentAdapter {\n //LLM文本对话地址\n @Override\n public String getChatCompletionsUrl(ChatAgentMessage chatAgentMessage) {\n return \"/compatible-mode/v1/chat/completions\";\n }\n //提交生成视频任务地址\n @Override\n public String getSubmitVideoTaskUrl(VideoAgentMessage videoAgentMessage){\n if(videoAgentMessage.getFirstFrameUrl() != null) {\n return \"/api/v1/services/aigc/image2video/video-synthesis\";\n }\n else {\n return \"/api/v1/services/aigc/video-generation/video-synthesis\";\n }\n }\n //语音生成地址\n @Override\n public String getGenAudioCompletionsUrl(AudioAgentMessage audioAgentMessage){\n return \"/api/v1/services/aigc/multimodal-generation/generation\";\n }\n\n /**\n * maas平台音频识别服务地址\n * @param audioSTTAgentMessage\n * @return\n */\n @Override\n\n public String getAudioSTTCompletionsUrl(AudioSTTAgentMessage audioSTTAgentMessage){\n return \"/api/v1/services/aigc/multimodal-generation/generation\";\n }\n //视频识别地址\n @Override\n public String getVideoVLCompletionsUrl(VideoVLAgentMessage videoVLAgentMessage) {\n return \"/v1/chat/completions\";\n }\n //图像识别地址\n @Override\n public String getImageVLCompletionsUrl(ImageVLAgentMessage imageVLAgentMessage) {\n return \"/compatible-mode/v1/chat/completions\";\n }\n //图像生成地址\n @Override\n public String getGenImageCompletionsUrl(ImageAgentMessage imageAgentMessage) {\n return \"/api/v1/services/aigc/multimodal-generation/generation\";\n }\n //查询和获取视频任务执行结果地址,执行任务后返回结果中,包含生成的视频url(用于下载),已下载视频本地访问地址\n @Override\n public String getVideoTaskResultUrl(VideoStoreAgentMessage videoStoreAgentMessage){\n return \"/api/v1/tasks/\"+videoStoreAgentMessage.getTaskId();\n }\n}\n\n```\n\n如果有涉及到相关模型协议规范的适配调整,可以重载父类的其他方法。\n\n### 11.2 注册适配器\n\n可以通过以下两种方法注册自定义适配器:\n\n#### 方法1 配置文件方式(推荐)\n\n```properties\nhttp.poolNames = deepseek,guiji,qwenvlplus,volcengine,jiutian,kimi,zhipu,minimax,hunyuan,custom\n\ncustom.http.maxTotal = 200\ncustom.http.defaultMaxPerRoute = 200\n# 配置maas平台地址\ncustom.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\n\ncustom.http.apiKeyId = sk-469f01d6d6db4fb2f4\n# 模型类型:AI智能体工具通过模型类型查找智能体模型对接适配器\ncustom.http.modelType = custom\n# 模型对接适配器配置\ncustom.http.agentAdapter=org.frameworkset.spi.ai.CustomAgentAdapter\n\n```\n\n说明:\n\n1. 定义一个名称为custom(名称可以根据实际情况定)的maas服务,并添加到http.poolNames中\n\n ```properties\n http.poolNames = deepseek,guiji,qwenvlplus,volcengine,jiutian,kimi,zhipu,minimax,hunyuan,custom\n ```\n\n\n\n2. 配置maas平台地址和apiKey\n\n ```properties\n custom.http.hosts=https://dashscope.aliyuncs.com\n ```\n\ncustom.http.apiKeyId = sk-469f01dbb5724e6dbb2f4\n ```\n\n\n\n3. 指定模型平台类型(可选)\n\n ```properties\n custom.http.modelType = custom\n ```\n\n 可以不指定,这时会使用hosts(maas平台地址)作为模型类型\n\n4. 模型对接适配器配置,配置定义好的自定义适配器类路径\n\n ```properties\n custom.http.agentAdapter=org.frameworkset.spi.ai.CustomAgentAdapter\n ```\n\n#### 方法2 代码API配置(不推荐)\n\n首先,还是要在配置文件中配置maas平台服务:只是不需要指定模型平台适配器\n\n```properties\nhttp.poolNames = deepseek,guiji,qwenvlplus,volcengine,jiutian,kimi,zhipu,minimax,hunyuan,custom\n\ncustom.http.maxTotal = 200\ncustom.http.defaultMaxPerRoute = 200\n# 配置maas平台地址\ncustom.http.hosts=https://dashscope.aliyuncs.com\n#基于apiKeyId认证配置(主要用于各种大模型服务对接认证)\n\ncustom.http.apiKeyId = sk-469f01d6d6db4fb2f4\n# 模型类型:AI智能体工具通过模型类型查找智能体模型对接适配器\ncustom.http.modelType = custom\n```\n\n然后在加载maas平台服务时,注册模型适配器:\n\n```java\nAgentAdapterFactory.registerAgentAdapter(\"custom\",CustomAgentAdapter.class);//注册模型平台适配器\nHttpRequestProxy.startHttpPools(\"application-stream.properties\");\n```\n## 十二、背压与缓冲控制\n\n```java\nFlux<ServerEvent> flux = aiAgent.streamChat(\"deepseek\", chatMsg)\n .limitRate(5) // 限制请求速率,每次最多 5 个元素\n .buffer(3); // 每 3 个元素缓冲一次\n\nflux.subscribe(bufferedEvents -> {\n for (ServerEvent event : bufferedEvents) {\n // 批量处理事件\n }\n});\n```\n\n---\n\n## 十三、完整示例:多轮智能问答助手\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.*;\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\nimport reactor.core.publisher.Flux;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.concurrent.CountDownLatch;\n\npublic class IntelligentAssistant {\n private static List<Map<String, Object>> sessionMemory = new ArrayList<>();\n private static AIAgent aiAgent = new AIAgent();\n \n static {\n HttpRequestProxy.startHttpPools(\"application-stream.properties\");\n }\n \n public static void chat(String message, boolean reset) throws InterruptedException {\n if (reset) {\n sessionMemory.clear();\n System.out.println(\"会话已重置\");\n }\n \n ChatAgentMessage chatMsg = new ChatAgentMessage();\n chatMsg.setModel(\"deepseek-chat\");\n chatMsg.setPrompt(message);\n chatMsg.setStream(true);\n chatMsg.setSessionMemory(sessionMemory);\n chatMsg.setSessionSize(50);\n chatMsg.setMaxTokens(8192);\n chatMsg.setTemperature(0.7);\n \n CountDownLatch latch = new CountDownLatch(1);\n StringBuilder answer = new StringBuilder();\n \n aiAgent.streamChat(\"deepseek\", chatMsg)\n .doOnNext(chunk -> {\n if (!chunk.isDone() && chunk.getData() != null) {\n System.out.print(chunk.getData());\n answer.append(chunk.getData());\n }\n })\n .doOnComplete(() -> {\n // 保存回答到会话记忆\n if (answer.length() > 0) {\n chatMsg.addSessionMessage(answer.toString());\n }\n System.out.println();\n latch.countDown();\n })\n .doOnError(error -> {\n System.err.println(\"错误: \" + error.getMessage());\n latch.countDown();\n })\n .subscribe();\n \n latch.await();\n }\n \n public static void main(String[] args) throws InterruptedException {\n chat(\"你好,我叫小明\", false);\n chat(\"我叫什么名字?\", false);\n chat(\"重置会话\", true);\n chat(\"我叫什么名字?\", false);\n }\n}\n```\n\n---\n\n## 十四、多智能体编排工作流\n\nbboss-ai-flow 模块提供了一套强大的智能体工作流编排能力,基于有向循环图实现多智能体协同。支持串行、并行、路由、条件分支、裁判评估等丰富的流程编排模式,能够快速构建复杂的多智能体系统。\n\n\n\n多智能体编排工作流底层基于[bboss jobflow](https://esdoc.bbossgroups.com/#/jobworkflow)实现(一套数据交换作业编排工作流)\n\n### 14.1 工作流核心组件\n\n#### 14.1.1工作流节点类型\n\n| 组件 | 说明 |\n|------|------|\n| `AIPlanAgent` | 工作流编排主控制器,负责构建和执行整个智能体工作流 |\n| `AINodeAgent` | 普通AI节点智能体,执行具体的AI任务 |\n| `UserNodeAgent` | 用户智能体,不引用上游父智能体的会话记忆,适合处理独立任务 |\n| `AIParrelAgent` | 并行智能体容器,内部多个子智能体并发执行 |\n| `AISequenceAgent` | 串行智能体容器,内部多个子智能体按顺序执行 |\n| `AIRouteAgent` | 基于LLM路由智能体,根据用户问题自动决策后续执行路径 |\n| AIKeywordsRouteAgent | 基于关键词数组路由智能体,根据用户问题和关键词自动决策后续执行路径 |\n| `AIJudgeAgent` | 裁判智能体,评估执行结果是否满足预期,内置节点及变量:#[input.query,scope=node]<br/>#[answer,scope=node] |\n| `StoreContext` | 会话存储上下文,支持内存和数据库两种持久化方式 |\n| StandaloneAgent | 独立智能体,不会接受上游消息,也不会向下游推送消息 |\n| `AIFlowNode` | 通用工作流节点,执行纯Java自定义逻辑,不调用大模型,可在工作流中加工和传递数据、记录流程过程数据<br/>,接口方法call带返回值 |\n| AIFlowNodeVoid | 通用工作流节点,执行纯Java自定义逻辑,不调用大模型,可在工作流中加工和传递数据、记录流程过程数据<br/>,接口方法call不需要返回值 |\n\n| 节点类型 | 继承类 | 特点 |\n| ------------ | --------------- | ------------------------------------------------------------ |\n| AI节点 | `AINodeAgent` | 标准AI智能体,自动引用父智能体会话记忆 |\n| 用户节点 | `UserNodeAgent` | 不引用上游父智能体记忆,适合独立任务或工具调用 |\n| 路由节点 | `AIRouteAgent` | 负责路由决策,不直接回答问题 |\n| 裁判节点 | `AIJudgeAgent` | 评估结果质量,输出判断结论 |\n| 通用流程节点 | `AIFlowNode` | 执行纯Java自定义逻辑,不调用大模型,可在工作流中加工和传递数据、记录流程过程数据 |\n\n\n\n#### 14.1.2 会话存储配置\n\n工作流支持将会话记录持久化到数据库、Clickhouse或内存中:\n\n##### 关系数据库会话存储设置\n\n```java\nimport org.frameworkset.spi.ai.store.StoreContext;\n\n// 关系数据库存储方式\nStoreContext storeContext = new StoreContext()\n .setSessionId(\"session_001\") // 会话ID\n .setUserId(\"user123\") // 用户ID\n .setRequestId(\"request123\") // 请求ID\n .setSessionSize(100) // 会话窗口大小\n .setStoreType(StoreContext.STORE_TYPE_DB) // 数据库存储\n .setDataSource(\"visualops\"); // 数据源名称\n\n\n```\n\n关系数据库数据源声明:\n\n```java\nSQLUtil.startPool(\"visualops\",//数据源名称\n \"com.mysql.cj.jdbc.Driver\",//oracle驱动\n \"jdbc:mysql://192.168.137.1:3306/bboss?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true\",//mysql链接串\n \"root\",\"123456\",//数据库账号和口令\n \"select 1 \" //数据库连接校验sql\n);\n```\n\n##### Clickhouse会话存储配置\n\n<a id=\"chatper14-122\"></a>\n\nClickhouse存储会话配置,生产环境推荐使用Clickhouse存储\n\n```java\nStoreContext storeContext = new StoreContext()\n.setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100) \n.setStoreType(StoreContext.STORE_TYPE_DB)\n .setRequestId(\"request123\")\n .setClickhouseCluster(\"vops_3shards_1replicas\")\n.setDataSource(\"visualops\")\n```\n\n\n\nClickhouse数据源声明\n\n```java\nDBConf tempConf = new DBConf();\ntempConf.setPoolname(\"visualops\");\ntempConf.setDriver(\"com.clickhouse.jdbc.ClickHouseDriver\");\ntempConf.setJdbcurl(\"jdbc:clickhouse:http://100.13.6.4:28123,100.13.6.7:28123,100.13.4.6:28123/visualops?b.enableBalance=true&b.balance=roundbin\");\ntempConf.setUsername(\"default\");\ntempConf.setPassword(\"123456\");\ntempConf.setValidationQuery(\"select 1 \");\n//tempConf.setTxIsolationLevel(\"READ_COMMITTED\");\ntempConf.setJndiName(\"jndi-visualops\" );\ntempConf.setInitialConnections(10);\ntempConf.setMinimumSize(10);\ntempConf.setMaximumSize(20);\ntempConf.setUsepool(true);\n\ntempConf.setShowsql(true);\nSQLManager. startPool(tempConf);\n```\n\nClickhouse数据源注意事项\n\n1)clickhouse 会话存储支持:通过框架自动创建表时,需要指定 clickhouse 集群名称,为每个集群节点定义名称为shard和replica的两个宏变量。\n\n通过以下方式设置clickhouse表集群名称。\n\n```java\nstoreContext.setClickhouseCluster(\"vops_3shards_1replicas\")\n```\n\nshard和replica两个宏变量配置,参考Clickhouse官方文档。\n\n如果通过框架自动建表,clickhouse 表集群名称一定要设置,否则报错,如果表已经提起创建创建好了,可以不用创建\n\n2)使用Clickhouse时,会话续问续答时,不会更新会话修改时间\n\n自行创建Clickhouse会话表时,参考文档:\n\nhttps://gitee.com/bboss/bboss-ai/blob/main/bboss-ai/src/main/java/org/frameworkset/spi/ai/store/db/clickhouse-agent.xml\n\n##### 内存会话存储配置\n\n```java\n// 内存存储方式(默认)\nStoreContext memoryContext = new StoreContext()\n .setSessionSize(50)\n .setStoreType(StoreContext.STORE_TYPE_MEMORY);\n```\n\n\n\n#### 14.1.3 工作流基本结构\n\n```java\nimport org.frameworkset.spi.ai.flow.*;\nimport org.frameworkset.spi.ai.model.ChatAgentMessage;\nimport org.frameworkset.spi.ai.model.LastSessionMessage;\nimport org.frameworkset.spi.ai.store.StoreContext;\n\n// 1. 定义会话消息\nChatAgentMessage chatAgentMessage = new ChatAgentMessage()\n .setModel(\"qwen3.6-plus\")\n .setMaas(\"qwenvlplus\")\n .setPrompt(\"介绍省份智能体\");\n\n// 2. 定义工作流智能体\nAIPlanAgent aiPlanAgent = new AIPlanAgent(storeContext)\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\")\n .setAgentId(\"workflowAgent\");\n\n// 3. 添加工作流节点...\n\n// 4. 执行工作流\nLastSessionMessage result = aiPlanAgent.chat();\n```\n\n\n\n### 14.2 串行任务编排\n\n串行智能体(`AISequenceAgent`)将多个子智能体按顺序执行,前一个智能体的输出会作为后续智能体的上下文输入。\n\n#### 14.2.1 同步串行工作流\n\n```java\n// 构建工作流智能体\nAIPlanAgent aiPlanAgent = new AIPlanAgent(new StoreContext()\n .setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100)\n .setStoreType(StoreContext.STORE_TYPE_DB).setRequestId(\"request123\")\n .setDataSource(\"visualops\"))\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\");\n\n// 添加第一个普通节点\nAIBaseNodeAgent introduceProvinces = new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setAgentName(\"介绍中国省份和直辖市\")\n .setAgentId(\"introduceProvinces\");\naiPlanAgent.addAgent(introduceProvinces);\n\n// 构建串行智能体\nAISequenceAgent sequenceAgent = new AISequenceAgent(aiPlanAgent)\n .setAgentId(\"sequenceAgent\")\n .setAgentName(\"串行任务节点\");\n\n// 按顺序添加子智能体\nsequenceAgent.addAgent(new AINodeAgent(\"用50字介绍湖南,并且和介绍中国省份和直辖市内容合并输出\")\n .setAgentId(\"jieshaohunan\")\n .setAgentName(\"用50字介绍湖南\"));\nsequenceAgent.addAgent(new AINodeAgent(\"用50字介绍湖北\")\n .setAgentId(\"jieshaohubei\")\n .setAgentName(\"用50字介绍湖北\"));\nsequenceAgent.addAgent(new AINodeAgent(\"用50字介绍江西\")\n .setAgentId(\"jieshaojiangxi\")\n .setAgentName(\"用50字介绍江西\"));\nsequenceAgent.addAgent(new AINodeAgent(\"将下面的文字翻译为英文(不要回答问题):用50字介绍江西\")\n .setAgentId(\"translate\")\n .setAgentName(\"将文字翻译为英文\"));\n\n// 将串行智能体加入主工作流\naiPlanAgent.addAgent(sequenceAgent);\n\n// 执行工作流\nLastSessionMessage lastSessionMessage = aiPlanAgent.chat();\nif (lastSessionMessage != null) {\n logger.info(\"结果: {}\", lastSessionMessage.getData());\n}\n```\n\n#### 14.2.2 流式串行工作流\n\n流式模式与同步模式结构一致,只需将 `chat()` 替换为 `chatStream()`:\n\n```java\nFlux<ServerEvent> flux = aiPlanAgent.chatStream();\n\nStringBuilder completeAnswer = new StringBuilder();\nCountDownLatch countDownLatch = new CountDownLatch(1);\n\nflux.doOnNext(event -> {\n if (event.getData() != null) {\n System.out.print(event.getData());\n }\n if (!event.isDone() && event.getData() != null) {\n completeAnswer.append(event.getData());\n } else {\n if (completeAnswer.length() > 0) {\n chatAgentMessage.addAgentResultSessionMessage(\n completeAnswer.toString(), event.getAgent());\n completeAnswer.setLength(0);\n }\n }\n})\n.doOnComplete(() -> {\n logger.info(\"\\n=== 流完成 ===\");\n countDownLatch.countDown();\n})\n.doOnError(error -> {\n logger.error(\"错误: \" + error.getMessage(), error);\n countDownLatch.countDown();\n})\n.subscribe();\n\ncountDownLatch.await();\n```\n\n#### 14.2.3 条件分支节点\n\n可以为节点添加条件触发器,控制节点是否执行:\n\n```java\nimport org.frameworkset.tran.jobflow.script.TriggerScriptAPI;\nimport org.frameworkset.tran.jobflow.context.NodeTriggerContext;\n\nIntegerCount integerCount = new IntegerCount();\n\n// 为 introduceProvinces 节点添加条件:仅第一次执行时触发\naiPlanAgent.addConditionFlowNode(introduceProvinces, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int i = integerCount.increament();\n return i == 1;\n }\n});\n```\n\n### 14.3 并行任务编排\n\n并行智能体(`AIParrelAgent`)将多个子智能体并发执行,所有子任务完成后自动聚合结果。\n\n#### 14.3.1 同步并行工作流\n\n```java\n// 构建工作流智能体\nAIPlanAgent aiPlanAgent = new AIPlanAgent(storeContext)\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\");\n\n// 添加初始节点\naiPlanAgent.addAgent(new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setAgentName(\"介绍中国省份和直辖市\")\n .setAgentId(\"introduceProvinces\"));\n\n// 构建并行智能体\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent)\n .setAgentId(\"aiParrelAgent\")\n .setAgentName(\"共享任务节点\");\n\n// 添加多个并行子智能体\naiParrelAgent.addAgent(new AINodeAgent(\"用50字介绍湖南\")\n .setAgentId(\"jieshaohunan\")\n .setAgentName(\"用50字介绍湖南\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍湖北\")\n .setAgentId(\"jieshaohubei\")\n .setAgentName(\"用50字介绍湖北\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍江西\")\n .setAgentId(\"jieshaojiangxi\")\n .setAgentName(\"用50字介绍江西\"));\n\n// 将并行智能体加入主工作流\naiPlanAgent.addAgent(aiParrelAgent);\n\n// 执行\nLastSessionMessage result = aiPlanAgent.chat();\n```\n\n#### 14.3.2 流式并行工作流\n\n```java\nFlux<ServerEvent> flux = aiPlanAgent.chatStream();\n\nStringBuilder completeAnswer = new StringBuilder();\nCountDownLatch countDownLatch = new CountDownLatch(1);\n\nflux.doOnSubscribe(subscription -> logger.info(\"开始订阅流...\"))\n .doOnNext(event -> {\n // 首条消息可添加扩展链接\n if (event.isFirst() && !event.isToolCallResponse()) {\n event.addExtendData(\"url\", \"https://www.bbossgroups.com\");\n event.addExtendData(\"title\", \"bboss官网\");\n }\n\n if (event.isDone()) {\n event.addExtendData(\"url\", \"https://www.bbossgroups.com\");\n event.addExtendData(\"title\", \"bboss官网\");\n }\n\n if (event.getData() != null) {\n System.out.print(event.getData());\n }\n\n if (event.isToolCallsType()) {\n System.out.println(\"\\n开始执行工具:\");\n }\n\n if (event.isDone() || event.finished()) {\n System.out.println();\n }\n\n if (!event.isDone()) {\n if (event.getData() != null) {\n completeAnswer.append(event.getData());\n }\n } else {\n if (completeAnswer.length() > 0) {\n chatAgentMessage.addAgentResultSessionMessage(\n completeAnswer.toString(), event.getAgent());\n completeAnswer.setLength(0);\n }\n }\n })\n .doOnComplete(() -> {\n logger.info(\"\\n=== 流完成 ===\");\n countDownLatch.countDown();\n })\n .doOnError(error -> {\n logger.error(\"错误: \" + error.getMessage(), error);\n countDownLatch.countDown();\n })\n .subscribe();\n\ncountDownLatch.await();\n```\n\n#### 14.3.3 并行结果聚合\n\n并行智能体执行完毕后,会自动调用 `buildResult()` 方法聚合所有子智能体的结果。默认格式为:\n\n```java\nagentId1:子智能体1的结果\nagentId2:子智能体2的结果\n```\n\n如需自定义聚合格式,可重载 `buildResult()` 方法:\n\n```java\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent) {\n @Override\n public String buildResult(List<LastSessionMessage> lastSessionMessages) {\n StringBuilder builder = new StringBuilder();\n for (LastSessionMessage msg : lastSessionMessages) {\n if (builder.length() > 0) {\n builder.append(\"\\n---\\n\");\n }\n builder.append(\"[\").append(msg.getMsgAgentId()).append(\"]\\n\")\n .append(msg.getData());\n }\n return builder.toString();\n }\n};\n```\n\n### 14.4 条件任务节点和触发器\n\n通过条件任务节点,可以在工作流或者并行/串行分支中,添加多个候选条件任务分支节点以及一个默认分支节点(可行),每个条件任务分支节点需指定特定的条件触发器。\n\n流程调度执行时,会从所以条件分支任务节点选择满足触发条件的任务分支节点执行,如果没有满足的节点,则会选择默认分支节点执行;如果既没有满足条件的条件分支节点,也没有设置默认分支节点,则会根据控制参数allCondtionNodeMatchedfailedContinue的值,来判别是否继续执行条件节点后面的任务节点(如果有),true,则执行,false不执行,直接终止当前条件节点所在流程或者并行/串行分支的执行。\n\n基于条件任务节点,可以实现:\n\n1. if-elseif-else条件决策树编排\n2. 有限循环图智能体流程编排\n3. 路由智能体编排\n\n#### 14.4.1 简单条件任务分支\n\n如果选择的智能体id为docAgent,则执行飞书文档智能体\n\n如果选择的智能体id为logAgent,则执行日志分析智能体\n\n如果没有选择具体的智能体,则执行默认智能体,直接回答用户问题\n\n```java\n//添加一个简单的决策智能体节点,在流程上下文中添加路由的agentId\nplanAgent.addAgent(new AIFlowNode() {\n /**\n * 由子类继承和实现\n *\n * @param jobFlowNodeExecuteContext\n * @return\n */\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n jobFlowNodeExecuteContext.addJobFlowContextData(\"routeChoice\",\"docAgent\");//模拟设置后续节点id\n return null;\n }\n });\n ToolsRegist feishuMcp = new FeishuMcpRegist(\"feishumcp\");\n planAgent.addConditionFlowNode(new UserNodeAgent(feishuMcp).setAgentId(\"docAgent\").setAgentName(\"飞书文档智能体\"),\n nodeTriggerContext -> {\n String agentId = (String) nodeTriggerContext.getFlowContextData(\"routeChoice\",true);\n \n if(agentId != null && agentId.equals(\"docAgent\")){\n return true;\n }\n return false;\n });\n planAgent.addConditionFlowNode(new UserNodeAgent().setAgentId(\"logAgent\").setAgentName(\"日志分析智能体\"),\n nodeTriggerContext -> {\n String agentId = (String) nodeTriggerContext.getFlowContextData(\"routeChoice\",true);\n \n if(agentId != null && agentId.equals(\"logAgent\")){\n return true;\n }\n return false;\n });\n//添加默认条件分支节点\n planAgent.addConditionFlowNode(new UserNodeAgent( ).setAgentId(\"defaultAgent\").setAgentName(\"默认智能体直接回答问题\"),true);\n//开始对话,执行对话流程,并返回会话结果\n Flux<ServerEvent> flux = planAgent.chatStream();\n```\n\n#### 14.4.2 条件分支后开启新条件分支\n\n可以通过以下系列addAnotherConditionJobFlowNodeAgent方法,在当前条件分支后开启一个新的条件分支:\n\n```java\npublic String addAnotherConditionJobFlowNodeAgent(AppendToParentAgent baseNodeAgent, NodeTrigger conditionNodeTrigger)\npublic String addAnotherConditionJobFlowNodeAgent(AppendToParentAgent baseNodeAgent, NodeTrigger conditionNodeTrigger,boolean defaultConditionNode) \npublic String addAnotherConditionJobFlowNodeAgent(boolean allCondtionNodeMatchedfailedContinue,AppendToParentAgent baseNodeAgent, TriggerScriptAPI conditionNodeTrigger)\npublic String addAnotherConditionJobFlowNodeAgent(boolean allCondtionNodeMatchedfailedContinue,AppendToParentAgent baseNodeAgent, TriggerScriptAPI conditionNodeTrigger,boolean defaultConditionNode) \n \n```\n\n注意调用addAnotherConditionJobFlowNodeAgent方法后,表示开启新的条件任务节点,并添加第一个分支,后续条件分支节点则通过addConditionFlowNode方法添加即可。\n\n#### 14.4.3 实现有向循环图\n\n基于条件分支节点,在通过addConditionFlowNode方法和addAnotherConditionJobFlowNodeAgent方法添加条件分支节点时,可以指定为当前流程或者并行、串行分支流程的前序节点(案例中为introduceProvinces)或者后序节点,从而实现有向循环图流程编排功能:\n\n**跳转到前序节点**\n\n```java\nAIPlanAgent planAgent = new AIPlanAgent(new StoreContext()\n .setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100) \n .setStoreType(StoreContext.STORE_TYPE_DB).setRequestId(\"request123\")\n .setDataSource(\"visualops\"))\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\")\n ;\nAIBaseNodeAgent introduceProvinces = new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\").setAgentName(\"介绍中国省份和直辖市\").setAgentId(\"introduceProvinces\");\nplanAgent.addAgent(introduceProvinces);\n//构建并行智能体\nAIParrelAgent aiParrelAgent = new AIParrelAgent(planAgent).setAgentId(\"aiParrelAgent\").setAgentName(\"并行智能体\");\naiParrelAgent.addAgent(new AINodeAgent(\"用50字介绍湖南,并且和介绍中国省份和直辖市内容合并输出\").setAgentId(\"jieshaohunan\").setAgentName(\"用50字介绍湖南\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍湖北\").setAgentId(\"jieshaohubei\").setAgentName(\"用50字介绍湖北\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍江西\").setAgentId(\"jieshaojiangxi\").setAgentName(\"用50字介绍江西\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"将下面的文字翻译为英文(不要回答问题):用50字介绍江西\").setAgentId(\"translate\").setAgentName(\"将文字翻译为英文\"));\nplanAgent.addAgent(aiParrelAgent);\nIntegerCount integerCount = new IntegerCount();\n//直接指向前序节点introduceProvinces,实现有条件循环,多循环一次,就结束循环,第一个参数为true,表示循环结束后继续执行流程后续节点\nplanAgent.addConditionFlowNode(true,introduceProvinces, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int i = integerCount.increament();\n if(i == 1) {\n return true;\n }\n else{\n return false;\n }\n }\n});\n//下面给流程增加了两个通用节点\nplanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点1。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n });\n\n planAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点2。\");\n logger.info(\"call 自定义节点2。customNode:{}\", jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\"));\n return null;\n }\n });\n```\n\n**跳转到后续节点**\n\n```java\n//定义会话实体:设置模型、maas平台,用户问题\nChatAgentMessage chatAgentMessage = new ChatAgentMessage() \n .setModel(model).setThinking(false)\n .setMaas(maas).setPrompt(prompt);\n\n//定义工作流智能体,设置会话存储机制为DB,设置DB数据源、当前会id以及用户id\n// 设置短期会话窗口\nAIPlanAgent planAgent = new AIPlanAgent(new StoreContext()\n .setSessionId(sessionId).setUserId(\"user123\").setSessionSize(100) \n .setStoreType(StoreContext.STORE_TYPE_DB).setRequestId(\"request123\")\n .setDataSource(\"visualops\"))\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\")\n ;\nAIBaseNodeAgent introduceProvinces = new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\").setAgentName(\"介绍中国省份和直辖市\").setAgentId(\"introduceProvinces\");\nplanAgent.addAgent(introduceProvinces);\nplanAgent.addAgent(new AIFlowNode() {\n /**\n * 由子类继承和实现\n *\n * @param jobFlowNodeExecuteContext\n * @return\n */\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n //生成一个10以内的随机整数,如果随机数是偶数则触发节点\n int randomInt = (int) (Math.random() * 10);\n logger.info(\"randomInt:{}\", randomInt);\n jobFlowNodeExecuteContext.addJobFlowContextData(\"randomInt\", randomInt);\n return null;\n }\n});\n//1.定义一个后序条件跳转节点\nAIFlowNode aiFlowNode = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode(满足条件时,会直接跳转到这个节点执行,绕过aiParrelAgent和循环跳转节点introduceProvinces,直接执行aiFlowNode后续流程节点)。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n};\n//2.添加后序条件跳转节点\nplanAgent.addConditionFlowNode(true,aiFlowNode, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int randomInt = (int) nodeTriggerContext.getFlowContextData(\"randomInt\");\n \n if (randomInt % 2 == 0) {\n return true;\n }\n return false;\n }\n});\n\nAIFlowNode aiFlowNode1 = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode1。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData1\");\n return null;\n }\n};\nplanAgent.addConditionFlowNode(aiFlowNode1, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n //生成一个10以内的随机整数,如果随机数是奇数则触发节点\n int randomInt = (int) nodeTriggerContext.getFlowContextData(\"randomInt\");\n if (randomInt % 2 != 0) {\n return true;\n }\n return false;\n }\n});\n\nAIFlowNode aiFlowNode2 = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode2。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData2\");\n return null;\n }\n};\nplanAgent.addConditionFlowNode(aiFlowNode2, true);\n//构建并行子智能体\nAIParrelAgent aiParrelAgent = new AIParrelAgent(planAgent).setAgentId(\"aiParrelAgent\").setAgentName(\"并行智能体\");\naiParrelAgent.addAgent(new AINodeAgent(\"用50字介绍湖南,并且和介绍中国省份和直辖市内容合并输出\").setAgentId(\"jieshaohunan\").setAgentName(\"用50字介绍湖南\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍湖北\").setAgentId(\"jieshaohubei\").setAgentName(\"用50字介绍湖北\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"用50字介绍江西\").setAgentId(\"jieshaojiangxi\").setAgentName(\"用50字介绍江西\"));\naiParrelAgent.addAgent(new UserNodeAgent(\"将下面的文字翻译为英文(不要回答问题):用50字介绍江西\").setAgentId(\"translate\").setAgentName(\"将文字翻译为英文\"));\nplanAgent.addAgent(aiParrelAgent);\nIntegerCount integerCount = new IntegerCount();\nplanAgent.addConditionFlowNode(true,introduceProvinces, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int i = integerCount.increament();\n if(i == 1) {\n return true;\n }\n else{\n return false;\n }\n }\n});\n//3.添加后序条件跳转节点到主流程中,前面的条件分支节点会直接跳转的本节点\nplanAgent.addAgent(aiFlowNode);\nplanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点3。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData3\");\n return null;\n }\n});\n\nplanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点4。customNode:{}\", jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\"));\n return null;\n }\n});\n\n\n//开始对话,执行对话流程,并返回会话结果 \nLastSessionMessage lastSessionMessage = planAgent.chat();\n\n//输出会话结果 \nif(lastSessionMessage != null) {\n String data = lastSessionMessage.getData();\n logger.info(\"serverEvent:{}\", data);\n}\n```\n\n说明:通过条件跳转到流程或者并行分支/串行分支后序节点,可以成功绕过部分中间节点\n\n```java\n//1.定义一个后序条件跳转节点\nAIFlowNode aiFlowNode = new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点customNode(满足条件时,会直接跳转到这个节点执行,绕过aiParrelAgent和循环跳转节点introduceProvinces,直接执行aiFlowNode后续流程节点)。\");\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n};\n\n//2.添加后序条件跳转节点\nplanAgent.addConditionFlowNode(true,aiFlowNode, new TriggerScriptAPI() {\n @Override\n public boolean needTrigger(NodeTriggerContext nodeTriggerContext) throws Exception {\n int randomInt = (int) nodeTriggerContext.getFlowContextData(\"randomInt\");\n \n if (randomInt % 2 == 0) {\n return true;\n }\n return false;\n }\n});\n\n。。。。。。中间节点。。。。。\n \n//3.添加后序条件跳转节点到主流程中,前面的条件分支节点会直接跳转的本节点\nplanAgent.addAgent(aiFlowNode);\n。。。。。。后续节点。。。。。\n```\n\n##### 14.4.3.1 关键说明\n\n通过以下方法添加条件跳转节点,如果条件不成立,会自动终止执行条件节点的后续节点执行:\n\n1)如果条件节点是在工作流也就是终止整个工作流\n\n2)如果条件节点是在串行分支中,则会终止串行分支节点执行\n\n```java\n//2.添加后序条件跳转节点\n planAgent.addConditionFlowNode(AppendToParentAgent aiAgent , TriggerScriptAPI conditionNodeTrigger)\n```\n\n通过以下方法添加条件跳转节点,如果条件不成立,当allCondtionNodeMatchedfailedContinue为true时,会自动执行条件节点的后续节点,不会终止条件节点所在的主干流程或者串行分支流程:\n\n```java\n//2.添加后序条件跳转节点\n planAgent.addConditionFlowNode(boolean allCondtionNodeMatchedfailedContinue,AppendToParentAgent aiAgent , TriggerScriptAPI conditionNodeTrigger)\n```\n\n有限循环图可以基于条件节点来实现的关键点,就在于**allCondtionNodeMatchedfailedContinue**参数,当allCondtionNodeMatchedfailedContinue取值为true时,我们指定条件节点为流程中或者串行分支中之前的节点,如果conditionNodeTrigger触发器判定条件成立,则继续跳转到导之前的节点重复执行,形成循环;如果判定条件不成立,则结束循环,继续执行后续流程节点。\n\n#### 14.4.4 注意事项\n\n- 条件节点只能在当前主干流程节点间跳转,不能跳转到分支复合节点内部\n\n- 条件节点只能在串行分支流程节点之间跳转,不能跳转分支复合节点内部或者分支外部\n\n- 条件节点只能在并行分支中并行分支流程节点之间跳转,不能跳转分支复合节点内部或者分支外部\n\n- 可以为条件节点设置条件触发器,节点可以加入到多个条件分支,每个分支都可以指定特定条件触发器,如果没有指定,则使用节点自身的条件触发器,条件节点必须指定自己的条件触发器\n\n### 14.5 路由智能体\n\n路由智能体(`AIRouteAgent`和AIKeywordsRouteAgent)基于条件任务节点实现,能够根据用户问题自动选择后续执行路径,实现智能分发。典型的**意图识别+路由分发**的AI架构模式,利用 LLM + 关键词规则,自动识别用户意图,然后决定走哪条处理分支,实现意图识别与路由分发机制。\n\n#### 14.5.1 同步路由工作流\n\n```java\nAIPlanAgent aiPlanAgent = new AIPlanAgent(storeContext)\n .setAgentMessage(chatAgentMessage)\n .setAgentName(\"工作流智能体\").setAgentId(\"workflowAgent\");\n\n// 构建路由规则智能体\naiPlanAgent.addAgent(new AIRouteAgent()\n .setAgentId(\"Router\")\n .setAgentName(\"路由规则智能体\")\n .setSystemPrompt(\"你是一个路由智能体。你的目标是将用户查询路由到正确的后续任务,注意你不需要回答用户的问题。\")\n .addRoutingChoice(\"weatherAgent\", \"查询城市天气,并给出穿衣出行建议\")\n .addRoutingChoice(\"docAgent\", \"操作飞书文档\")\n);\n\n// 构建天气查询智能体\naiPlanAgent.addRouteChoiceAgent(new UserNodeAgent(new MCPToolsRegist(\"visualops\"))\n .setAgentId(\"weatherAgent\")\n .setAgentName(\"天气查询智能体\"));\n\n// 构建飞书文档操作智能体\nToolsRegist feishuMcp = new FeishuMcpRegist(\"feishumcp\");\naiPlanAgent.addRouteChoiceAgent(new UserNodeAgent(feishuMcp)\n .setAgentId(\"docAgent\")\n .setAgentName(\"飞书文档智能体\"));\n\n// 构建默认智能体:当用户问题匹配不上时执行\naiPlanAgent.addDefaultRouteChoiceAgent(\n new AINodeAgent().setAgentId(\"defaultAgent\").setAgentName(\"默认智能体\"));\n\n// 构建裁判智能体:判断是否回答了问题\naiPlanAgent.addAgent(new AIJudgeAgent(\"评估结果是否回答了问题,回答请回复:是,否则回复:否\")\n .setAgentId(\"judgeAgent\")\n .setAgentName(\"评估智能体\"));\n\n// 构建条件节点:根据裁判结果决定是否创建飞书文档\naiPlanAgent.addAgent(\n new AINodeAgent(\"将结果创建为飞书文档\", feishuMcp)\n .setAgentId(\"createDocAgent\")\n .setAgentName(\"飞书文档创建智能体\"),\n nodeTriggerContext -> {\n String judgeResult = (String) nodeTriggerContext\n .getFlowContextData(\"judgeAgent.judgeResult\");\n return \"是\".equals(judgeResult);\n });\n\n// 执行\nLastSessionMessage result = aiPlanAgent.chat();\n```\n\n#### 14.5.2 流式路由工作流\n\n流式路由与同步路由结构一致,只需调用 `chatStream()`:\n\n```java\nFlux<ServerEvent> flux = aiPlanAgent.chatStream();\n// ... 流式处理逻辑与并行流式示例相同\n```\n\n#### 14.5.3 关键词路由智能体\n\n```java\n planAgent.addAgent(new AIKeywordsRouteAgent()\n\t\t\t\t.addRoutingChoice(\"bothAgent\", new String[]{\"酒店和机票\"}, \"用户需要同时预定酒店和机票\")\n\t\t\t\t.addRoutingChoice(\"hotelAgent\", new String[]{\"酒店\"},\"用户只需要预定酒店\")\n\t\t\t\t.addRoutingChoice(\"flightAgent\", new String[]{\"航班\",\"机票\"}, \"用户只需要预定机票\")\n\t\t\t\t\n .setAgentId(\"bookingRouter\").setAgentName(\"预定路由智能体\")\n .setSystemPrompt(\"你是一个行程预定路由智能体。请分析用户的问题,判断用户需要预定什么,注意你不需要直接回答用户的问题,只需要做路由判断,使用json格式返回匹配的智能体信息\") \n \n );\n```\n\nAIKeywordsRouteAgent 是轻量级的关键词匹配方案,适合意图明确、关键词边界清晰的场景(如本例中的\"酒店\"、\"机票\")。\n\n### 14.6 通用流程节点\n\nbboss提供了两种类型通用节点:AIFlowNode和AIFlowNodeVoid(二者区别:后者call方法无返回值,视情况选取),通用流程节点用于在工作流中执行纯 Java 代码逻辑,不调用大模型。适合数据转换、参数校验、外部接口调用、数据预处理、记录用户输入和创建用户会话等场景。\n\n记录用户输入信息和创建会话:\n\n```java\n planAgent.addAgent(new AIFlowNodeVoid(){\n \n @Override\n public void call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n // ===== 2. 加载历史会话消息,如果会话不存在,则创建 =====\n planAgent.loadSessionMemory( question, domain);\n TraceMessage traceMessage = new TraceMessage(); \n //记录用户输入的原始问题\n traceMessage.setMessage(Map.of(\"question\", question,\"role\",SessionMessage.MESSAGE_TYPE_USERINPUTMESSAGE_NAME));\n //其他用户上传的附件材料信息可以放到metaData中,也可以直接放到上面的消息中\n// traceMessage.setMetaData(Map.of(\"documents\", new ArrayList<>()));\n traceMessage.setStartTime(System.currentTimeMillis());\n// traceMessage.setEndTime(System.currentTimeMillis());\n planAgent.recordTraceMessage(traceMessage);\n \n \n \n }\n });\n```\n\n其他应用案例:\n\n \n\n```java\nimport org.frameworkset.spi.ai.flow.AIFlowNode;\nimport org.frameworkset.tran.jobflow.context.JobFlowNodeExecuteContext;\n\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点1。\");\n // 向工作流上下文写入数据\n jobFlowNodeExecuteContext.addJobFlowContextData(\"customNode\", \"customNodeData\");\n return null;\n }\n});\n\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点2。\");\n // 从工作流上下文读取数据\n Object data = jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\");\n logger.info(\"customNode:{}\", data);\n return null;\n }\n});\n\naiPlanAgent.addAgent(new AIFlowNodeVoid() {\n @Override\n public void call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n logger.info(\"call 自定义节点2。\");\n // 从工作流上下文读取数据\n Object data = jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\");\n logger.info(\"customNode:{}\", data);\n \n});\n```\n\n通用流程节点通过 `JobFlowNodeExecuteContext` 实现与上下游节点的数据交互:\n\n- `addJobFlowContextData(key, value)`:向工作流上下文写入键值对数据\n- `getJobFlowContextData(key)`:从工作流上下文读取数据\n\n### 14.7 评估智能体\n\n```java\n//构建裁判智能体:判断是否回答了问题,可以指定评估提示词,通过变量一样问题和问题答案\naiPlanAgent.addAgent(new AIJudgeAgent(\"评估结果是否回答了问题:\\n#[input.query,scope=node]\\n# 问题答案:\\n#[answer,scope=node],回答请回复:是,否则回复:否\").setAgentId(\"judgeAgent\").setAgentName(\"评估智能体\"));\n\n//构建裁判智能体:如果不指定提示词,则使用默认提示词\n\naiPlanAgent.addAgent(new AIJudgeAgent().setAgentId(\"judgeAgent\").setAgentName(\"评估智能体\"));\n```\n\n\n\n### 14.8 智能体配置参数\n\n### 14.9 工作流组合模式\n\n串行和并行智能体可以相互嵌套,构建复杂的工作流:\n\n```java\n// 串行智能体中嵌入并行智能体\nAISequenceAgent sequenceAgent = new AISequenceAgent(aiPlanAgent);\n\nAIParrelAgent innerParrel = new AIParrelAgent(aiPlanAgent);\ninnerParrel.addAgent(new AINodeAgent(\"任务A\"));\ninnerParrel.addAgent(new AINodeAgent(\"任务B\"));\n\nsequenceAgent.addAgent(innerParrel); // 并行作为串行的一个步骤\nsequenceAgent.addAgent(new AINodeAgent(\"任务C\"));\n\naiPlanAgent.addAgent(sequenceAgent);\n\n// 并行智能体中嵌入串行智能体\nAIParrelAgent parrelAgent = new AIParrelAgent(aiPlanAgent);\n\nAISequenceAgent innerSequence = new AISequenceAgent(aiPlanAgent);\ninnerSequence.addAgent(new AINodeAgent(\"步骤1\"));\ninnerSequence.addAgent(new AINodeAgent(\"步骤2\"));\n\nparrelAgent.addAgent(innerSequence);\nparrelAgent.addAgent(new AINodeAgent(\"独立任务\"));\n\naiPlanAgent.addAgent(parrelAgent);\n```\n\n### 14.10 智能体工作流变量体系\n\n智能体工作流支持丰富的变量传递机制,支持在智能体之间传递数据,并在 Prompt 中动态引用变量。\n\n#### 14.10.1 变量概述\n\n工作流中的变量用于实现智能体之间的数据共享和传递,主要包含以下使用场景:\n\n- **输出变量**:将某个智能体的执行结果保存为变量,供后续节点使用\n- **Prompt 变量引用**:在提示词中通过占位符动态注入其他节点的输出结果\n- **内置变量**:框架自动注入的流程级变量,如用户输入、系统提示等\n- **上下文变量**:通过 `JobFlowNodeExecuteContext` 或 `NodeTriggerContext` 在代码层面读写\n- **智能体参数变量**:通过Agent.addParam(key,value)方式添加,主要用于非工作流场景,添加的变量,可以智能体提示词和系统提示词中使用\n\n#### 14.10.2 设置输出变量\n\n通过 `setOutputVaribleName` 方法将智能体的输出结果保存为指定名称的变量,并可指定作用域:\n\n```java\nimport org.frameworkset.spi.ai.model.AIFlowConst;\n\naiPlanAgent.addAgent(new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setOutputVaribleName(\"provinces\", AIFlowConst.AIFLOW_VAR_SCOPE_FLOW)\n .setAgentName(\"介绍中国省份和直辖市\")\n .setAgentId(\"introduceProvinces\"));\n```\n\n| 参数 | 说明 |\n|------|------|\n| `varName` | 变量名称,后续通过该名称引用 |\n| `scope` | 变量作用域,可选 `AIFlowConst.AIFLOW_VAR_SCOPE_FLOW`(全局)、`AIFLOW_VAR_SCOPE_NODE`(节点级)、`AIFLOW_VAR_SCOPE_CONTAINER`(容器级) |\n\n#### 14.10.3 变量作用域\n\n| 作用域常量 | 说明 |\n|-----------|------|\n| `AIFLOW_VAR_SCOPE_FLOW` | 全局作用域,整个工作流均可访问 |\n| `AIFLOW_VAR_SCOPE_NODE` | 节点作用域,仅在当前节点内有效 |\n| `AIFLOW_VAR_SCOPE_CONTAINER` | 容器作用域,仅在当前串行/并行容器内有效 |\n\n#### 14.10.4 在 Prompt 中使用变量\n\n在智能体的提示词中,通过 `#[变量名,scope=作用域]` 的语法引用已定义的变量:\n\n```java\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent)\n .setAgentId(\"aiParrelAgent\")\n .setAgentName(\"共享任务节点\");\n\n// 在 Prompt 中引用 flow 作用域的 provinces 变量\naiParrelAgent.addAgent(new AINodeAgent(\n \"同时结合中国省份特点:\\r\\n#[provinces,scope=flow],\\r\\n用300字介绍湖南\")\n .setAgentId(\"jieshaohunan\")\n .setAgentName(\"用50字介绍湖南\"));\n```\n\n**变量引用语法:**\n\n```\n#[变量名,scope=作用域]\n```\n\n其中 `scope` 可选值为:`flow`、`node`、`container`,需与定义变量时指定的作用域一致。\n\n#### 14.10.5 在代码中获取变量\n\n在条件触发器或通用流程节点中,可通过上下文对象获取变量值:\n\n**条件触发器中获取变量:**\n```java\naiPlanAgent.addAgent(new AINodeAgent(\"将结果创建为飞书文档\", feishuMcp)\n .setAgentId(\"createDocAgent\")\n .setAgentName(\"飞书文档创建智能体\"),\n nodeTriggerContext -> {\n // 获取 judgeAgent 智能体的裁判结果\n String judgeResult = (String) nodeTriggerContext\n .getFlowContextData(\"judgeAgent.judgeResult\");\n return \"是\".equals(judgeResult);\n });\n```\n\n**通用流程节点中获取变量:**\n```java\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext jobFlowNodeExecuteContext) {\n Object data = jobFlowNodeExecuteContext.getJobFlowContextData(\"customNode\");\n return data;\n }\n});\n```\n\n#### 14.10.6 内置变量\n\n框架自动维护以下内置变量:\n\n| 内置变量 | 说明 |\n|---------|------|\n| `input.query` | 用户输入的原始问题(Prompt) |\n| `input.system` | 系统提示词(System Prompt) |\n| `judgeAgent.judgeResult` | `AIJudgeAgent` 裁判节点的评估结果 |\n\n#### 14.10.8 智能体参数变量\n\n在非工作流智能体中,亦可以使用外部资源变量和智能体参数变量,示例如下\n\n智能体参数变量:\n\n```java\n //定义提示词\n String message = \"介绍一下bboss jobflow #[version]\";\nAIAgent aiAgent = new AIAgent();\naiAgent.addParam(\"version\",\"7.5.7\");//添加version参数,用于在问题中替换#[version]\n```\n\n外部资源:参考章节[14.11 加载外部资源提示词](#chapter1411)\n\n#### 14.10.7 完整示例\n\n以下示例演示了从变量定义、Prompt 引用到代码获取的完整流程:\n\n```java\n// 1. 定义输出变量:将省份介绍结果保存为 flow 级变量\naiPlanAgent.addAgent(new AINodeAgent(\"用200字介绍中国有多少个省份和直辖市\")\n .setOutputVaribleName(\"provinces\", AIFlowConst.AIFLOW_VAR_SCOPE_FLOW)\n .setAgentId(\"introduceProvinces\"));\n\n// 2. 在并行智能体中通过 Prompt 引用变量\nAIParrelAgent aiParrelAgent = new AIParrelAgent(aiPlanAgent);\naiParrelAgent.addAgent(new AINodeAgent(\n \"结合省份特点:#[provinces,scope=flow],用300字介绍湖南\")\n .setAgentId(\"jieshaohunan\"));\n\n// 3. 通用流程节点处理变量\naiPlanAgent.addAgent(new AIFlowNode() {\n @Override\n public Object call(JobFlowNodeExecuteContext ctx) {\n String provinces = (String) ctx.getJobFlowContextData(\"provinces\");\n logger.info(\"获取到省份数据:{}\", provinces);\n ctx.addJobFlowContextData(\"processed\", true);\n return null;\n }\n});\n```\n\n### 14.11 加载外部资源提示词\n\n<a id=\"chapter1411\"></a>\n\n智能体支持三种加载外部资源提示词的方法:\n\n1)加载外部文件中的提示词\n\n2)加载classpath下文件中的提示词\n\n3)加载url中提供的提示词\n\n分别举例如下:\n\n```java\n String message = \"#[prompt.txt,type=resource,charset=UTF-8]\";\n String message = \"#[prompt.txt,type=resource]\";\n String message = \"#[http://localhost:85/prompt.txt,type=url,charset=UTF-8]\";\n String message = \"#[C:\\\\workspace\\\\bbossgroups\\\\bboss-ai\\\\bboss-ai-flow\\\\src\\\\test\\\\resources\\\\prompt.txt,type=file,charset=UTF-8]\";\n```\n\n参考文档:https://esdoc.bbossgroups.com/#/bboss-ai-prompt-resource\n\n### 14.12 流程调度\n\nbboss提供了智能体流程一次性执行和定期调度执行机制,可以设置节假日忽略执行策略,先看一个完整示例:\n\n```java\nprivate static void initDeepseekService(){\n \n HttpRequestProxy.startHttpPools(\"application.properties\");//启动模型服务服务\n\n }\n public static void main(String[] args){\n //初始化Deepseek服务\n initDeepseekService();\n ChatAgentMessage chatAgentMessage = new ChatAgentMessage();\n chatAgentMessage.setModel(\"deepseek-chat\").setMaas(\"deepseek\");\n\n //构建流程\n AIPlanAgent planAgent = new AIPlanAgent(new StoreContext().setStoreType(StoreContext.STORE_TYPE_MEMORY).setSessionSize(100));\n planAgent.setAgentName(\"Deepseek写诗-评价诗词流程\")\n .setAgentId(\"测试id\").setAgentMessage(chatAgentMessage);\n \n HolidayJobFlowScheduleConfig jobFlowScheduleConfig = new HolidayJobFlowScheduleConfig();\n jobFlowScheduleConfig.setDelay(1000L);//延期1秒执行\n jobFlowScheduleConfig.setFixedRate(true);\n jobFlowScheduleConfig.setPeriod(30000L);//每隔30秒执行\n jobFlowScheduleConfig.addCustomHoliday(\"2026-06-10\");//设定某天不执行,可以调用多次添加多天不执行\n \n planAgent.setJobFlowScheduleConfig(jobFlowScheduleConfig);\n\n planAgent.addAgent(new AINodeAgent(\"模仿李白的风格写一首七律.飞机!\").setSystemPrompt(\"你是一位唐代诗人.\").setAgentId(\"1\").setAgentName(\"Deepseek写诗-评价诗词流程\").setAgentOutput(new AgentOutput() {\n @Override\n public void output(ServerEvent message) {\n logger.info(\"--------诗歌内容---------\\n{}\",message.getData());\n }\n }));\n\n planAgent.addAgent(new AINodeAgent(\"帮忙评估上述诗词的意境\").setAgentId(\"2\").setAgentName(\"Deepseek-chat-分析诗\").setAgentOutput(new AgentOutput() {\n @Override\n public void output(ServerEvent message) {\n logger.info(\"--------诗歌评价---------\\n{}\",message.getData());\n }\n }));\n\n\n planAgent.chat();\n \n \n\n }\n```\n\nbboss默认采用一次性执行智能体编排工作流,如需周期性执行,则进行以下配置:\n\n\n```java\n HolidayJobFlowScheduleConfig jobFlowScheduleConfig = new HolidayJobFlowScheduleConfig();\n jobFlowScheduleConfig.setDelay(1000L);//延期1秒执行\n jobFlowScheduleConfig.setFixedRate(true);\n jobFlowScheduleConfig.setPeriod(30000L);//每隔30秒执行\n jobFlowScheduleConfig.addCustomHoliday(\"2026-06-10\");//设定某天不执行,可以调用多次添加多天不执行\n jobFlowScheduleConfig.setSkipSunday(true);\n planAgent.setJobFlowScheduleConfig(jobFlowScheduleConfig);\n```\n如果需要跳过节假日不执行,则进行如下设置:\n\n```java\njobFlowScheduleConfig.addCustomHoliday(\"2026-06-10\");//设定某天不执行,可以调用多次添加多天不执行\n/**\n * 设置跳过星期六\n \n */\njobFlowScheduleConfig.skipSaturday() \n\n/**\n * 设置跳过星期天\n * @return\n */\njobFlowScheduleConfig.skipSunday() \n\n/**\n * 设置跳过周末(星期六和星期天)\n * @return\n */\njobFlowScheduleConfig.skipWeekends() \n\n/**\n * 设置跳过元旦(1月1日)\n * @return\n */\njobFlowScheduleConfig.skipNewYearsDay() \n\n/**\n * 设置跳过劳动节(5月1日)\n * @return\n */\njobFlowScheduleConfig.skipLaborDay() \n\n/**\n * 设置跳过端午节\n * @return\n */\njobFlowScheduleConfig.skipDragonBoatFestival() \n\n/**\n * 设置跳过中秋节\n * @return\n */\njobFlowScheduleConfig.skipMidAutumnFestival() \n\n/**\n * 设置跳过国庆节(10月1日)\n * @return\n */\njobFlowScheduleConfig.skipNationalDay() \n\n/**\n * 设置跳过春节\n * @return\n */\njobFlowScheduleConfig.skipSpringFestival() \n\n/**\n * 设置跳过所有内置节假日(元旦、劳动节、端午节、中秋节、国庆节、春节)及周末\n * @return\n */\njobFlowScheduleConfig.skipAllHolidays() \n```\n\n### 14.13 自定义消息推送\n\n```java\nServerEvent serverEvent = new ServerEvent();//向客户端推送拒答信息\nserverEvent.setType(ServerEvent.TYPE_REFUSAL);\nserverEvent.setData(msg);\nserverEvent.setConfidence(confidence);\nserverEvent.setDone(true);//告诉前端流已经输出完毕\ngetAgentFluxSink().next(serverEvent);\n```\n\n### 14.14 流程轨迹记录\n\n```java\n// ===== 2. 加载历史会话消息,如果会话不存在,则创建 =====\n\t\t\t\t\tplanAgent.loadSessionMemory( question, domain);\n\t\t\t\t\tTraceMessage traceMessage = new TraceMessage();\t\t\n\t\t\t\t\t//记录用户输入的原始问题\n\t\t\t\t\ttraceMessage.setMessage(Map.of(\"question\", question,\"role\",SessionMessage.MESSAGE_TYPE_USERINPUTMESSAGE_NAME));\n\t\t\t\t\t//其他用户上传的附件材料信息可以放到metaData中,也可以直接放到上面的消息中\n//\t\t\t\t\ttraceMessage.setMetaData(Map.of(\"documents\", new ArrayList<>()));\n\t\t\t\t\ttraceMessage.setStartTime(System.currentTimeMillis());\n//\t\t\t\t\ttraceMessage.setEndTime(System.currentTimeMillis());\n\t\t\t\t\tplanAgent.recordTraceMessage(traceMessage);\n```\n\n\n\n### 14.15 停止智能体工作流\n\n可以通过以下方法停止智能体工作流的执行:\n\n```java\naiPlanAgent.shutdown();\n```\n\n## 十五、向量模型与Rerank\n\nbboss AI 内置了文本向量化(Embedding)和重排序(Rerank)能力,可配合 Elasticsearch 等向量数据库实现检索增强生成(RAG)。\n\n### 15.1 向量模型 Embedding\n\nbboss-ai 内置了统一的向量模型调用能力,通过 `AIAgent.embedding(EmbeddingMessage)` 方法即可将文本转换为高维向量。\n\n#### 基本用法\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.EmbeddingMessage;\n\npublic float[] text2embedding(String text) {\n EmbeddingMessage embeddingMessage = new EmbeddingMessage();\n embeddingMessage.setInput(text); // 设置将要向量化的数据\n embeddingMessage.setModel(\"bge-m3\"); // 指定向量模型名称\n embeddingMessage.setMaas(\"embedding_model\"); // 指定 MaaS 平台服务名\n\n AIAgent agent = new AIAgent();\n float[] embedding = agent.embedding(embeddingMessage);\n if (embedding == null) {\n throw new AIRuntimeException(\"Embedding failed: response is null\");\n }\n return embedding;\n}\n```\n\n#### 关键参数说明\n\n| 参数 | 说明 |\n|------|------|\n| `input` | 待向量化的文本内容 |\n| `model` | 向量模型名称,如 `bge-m3`、`bge-large-zh-v1.5` 等 |\n| `maas` | MaaS 平台服务配置名,对应 `application.properties` 中的服务定义 |\n\n> **提示**:向量模型服务需在 `application.properties` 中预先配置好对应的 MaaS 服务地址和认证信息。\n\n---\n\n### 15.2 Rerank 重排序\n\n在 RAG(检索增强生成)场景中,向量检索和 BM25 文本检索返回的结果往往存在排序差异。`AIAgent.rerank(RerankMessage)` 方法调用专业的 Rerank 模型对候选文档进行重新排序,显著提升检索结果的相关性。\n\n#### 基本用法\n\n```java\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.RerankMessage;\nimport org.frameworkset.spi.ai.model.RerankDocument;\nimport org.frameworkset.spi.ai.model.RerankedDocument;\n\npublic List<RerankedDocument> rerankDocuments(String query, List<RerankDocument> candidates) {\n RerankMessage rerankMessage = new RerankMessage();\n rerankMessage.setMaas(\"agw\"); // MaaS 服务名\n rerankMessage.setModel(\"bge-reranker-v2-m3\"); // Rerank 模型\n rerankMessage.setRerankDocuments(candidates); // 候选文档列表\n rerankMessage.setQuery(query); // 用户查询\n rerankMessage.setReturnDocuments(false); // 是否返回原始文本\n rerankMessage.setTopK(10); // 最多返回前 10 个最相关文档\n rerankMessage.setRelevanceScore(0.5d); // 只召回相似度大于该值的文档\n\n AIAgent aiAgent = new AIAgent();\n List<RerankedDocument> rerankedDocuments = aiAgent.rerank(rerankMessage);\n return rerankedDocuments;\n}\n```\n#### RerankMessage 数据结构\n\n| 字段 | 类型 | 说明 |\n|------|------|------|\n| `maas` | `String` | MaaS 平台服务配置名,对应 `application.properties` 中的服务定义 |\n| `model` | `String` | Rerank 模型名称,如 `bge-reranker-v2-m3` 等 |\n| `query` | `String` | 用户查询语句,用于与候选文档计算相关性 |\n| `rerankDocuments` | `List<RerankDocument>` | 候选文档列表,需预先通过向量检索或 BM25 检索构建 |\n| `returnDocuments` | `boolean` | 是否在结果中返回原始文档内容;设为 `true` 可在结果中直接获取原始文本,设为 `false` 则仅返回分数和索引,减少数据传输。 |\n| `topK` | `int` | 最多返回的文档数量,只召回相似度最高的前 `topK` 个文档 |\n| `relevanceScore` | `double` | 相关性分数阈值,只召回相似度大于该值的文档 |\n\n#### RerankDocument 数据结构\n\n```java\nRerankDocument doc = new RerankDocument();\ndoc.setDocument(\"文档原始文本内容\"); // 设置文档内容\ndoc.setVectorScore(0.85f); // 来自向量检索的分数(可选)\ndoc.setBm25Score(12.5f); // 来自 BM25 检索的分数(可选)\ndoc.setMetadata(metaMap); // 附加元数据(可选)\n```\n\n#### RerankedDocument 返回结构\n\n| 字段 | 说明 |\n|------|------|\n| `index` | 原始文档在候选列表中的位置 |\n| `relevanceScore` | Rerank 模型计算的相关性分数 |\n| `document` | 原始文档内容(当 `returnDocuments=true` 时返回) |\n\n\n---\n\n### 15.3 RAG 实战:混合检索 + Rerank\n\n以下是一个完整的 RAG 检索流程示例,演示如何结合 Elasticsearch KNN 向量检索、BM25 文本检索和 Rerank 重排序,实现高质量的知识库问答。\n\n#### 15.3.1 初始化与启动\n\n```java\nimport org.frameworkset.elasticsearch.boot.ElasticSearchBoot;\nimport org.frameworkset.spi.remote.http.HttpRequestProxy;\n\n// 启动 Elasticsearch 客户端和 HTTP 连接池\nElasticSearchBoot.boot(\"application-stream.properties\");\nHttpRequestProxy.startHttpPools(\"application-stream.properties\");\n```\n\n#### 15.3.2 完整检索流程\n\n```java\nimport org.frameworkset.elasticsearch.ElasticSearchHelper;\nimport org.frameworkset.elasticsearch.client.ClientInterface;\nimport org.frameworkset.elasticsearch.entity.ESDatas;\nimport org.frameworkset.elasticsearch.entity.MetaMap;\nimport org.frameworkset.spi.ai.AIAgent;\nimport org.frameworkset.spi.ai.model.*;\n\npublic void searchVectorAndRerank() {\n ClientInterface clientUtil = ElasticSearchHelper\n .getConfigRestClientUtil(\"esmapper/knowledge.xml\");\n String query = \"Spring AOP\";\n\n // 1. 向量检索(KNN)\n Map vectorParams = new LinkedHashMap();\n vectorParams.put(\"embedding\", text2embedding(query));\n vectorParams.put(\"k\", 100);\n vectorParams.put(\"size\", 50);\n vectorParams.put(\"similarity\", 0.5);\n ESDatas<MetaMap> vectorDatas = clientUtil.searchList(\n \"/knowledge_chunks/_search\", \"searchWithScore\",\n vectorParams, MetaMap.class);\n List<MetaMap> vectorResults = vectorDatas.getDatas();\n\n // 2. BM25 文本检索\n Map bm25Params = new LinkedHashMap();\n bm25Params.put(\"query\", query);\n bm25Params.put(\"size\", 50);\n bm25Params.put(\"is_active\", true);\n ESDatas<MetaMap> bm25Datas = clientUtil.searchList(\n \"/knowledge_chunks/_search\", \"searchBM25\",\n bm25Params, MetaMap.class);\n List<MetaMap> bm25Results = bm25Datas.getDatas();\n\n // 3. 合并结果(去重)\n Map<String, RerankDocument> mergedMap = new LinkedHashMap<>();\n if (vectorResults != null) {\n for (MetaMap metaMap : vectorResults) {\n String chunkId = (String) metaMap.get(\"chunk_id\");\n RerankDocument doc = new RerankDocument();\n doc.setDocument((String) metaMap.get(\"content\"));\n doc.setVectorScore(metaMap.getScore());\n doc.setMetadata(metaMap);\n mergedMap.put(chunkId, doc);\n }\n }\n if (bm25Results != null) {\n for (MetaMap metaMap : bm25Results) {\n String chunkId = (String) metaMap.get(\"chunk_id\");\n RerankDocument doc = mergedMap.get(chunkId);\n if (doc == null) {\n doc = new RerankDocument();\n doc.setDocument((String) metaMap.get(\"content\"));\n doc.setBm25Score(metaMap.getScore());\n doc.setMetadata(metaMap);\n mergedMap.put(chunkId, doc);\n } else {\n doc.setBm25Score(metaMap.getScore());\n }\n }\n }\n\n // 4. Rerank 重排序\n if (mergedMap != null && mergedMap.size() > 0) {\n List<RerankDocument> rerankDatas = new ArrayList<>(mergedMap.values());\n\n RerankMessage rerankMessage = new RerankMessage();\n rerankMessage.setMaas(\"agw\");\n rerankMessage.setModel(\"bge-reranker-v2-m3\");\n rerankMessage.setRerankDocuments(rerankDatas);\n rerankMessage.setQuery(query);\n rerankMessage.setReturnDocuments(false);\n rerankMessage.setTopK(3); // 最多返回前 3 个最相关文档\n rerankMessage.setRelevanceScore(0.8d); // 只召回相似度大于 0.8 的文档\n\n AIAgent aiAgent = new AIAgent();\n List<RerankedDocument> rerankedDocuments = aiAgent.rerank(rerankMessage);\n\n // 按 relevanceScore 排序后的最终结果就是高质量检索结果\n logger.info(\"Rerank 结果: {}\", JsonUtil.object2jsonPretty(rerankedDocuments));\n }\n}\n```\n\n#### 流程说明\n\n1. **向量检索**:通过 KNN 搜索获取语义相关的文档片段\n2. **BM25 检索**:通过关键词匹配获取文本相关的文档片段\n3. **结果合并**:以 `chunk_id` 为键去重合并,同时保留两种检索的分数\n4. **Rerank 排序**:调用 Rerank 模型对合并后的候选集进行精细排序,返回按相关性排列的最终结果\n\n> **混合检索方案**:Elasticsearch 8.x 也支持原生的混合检索(`knn` + `query` + `rank: { rrf }`),可通过 `searchHybrid` DSL 一步完成向量+BM25+RRF 融合,详见下面索引配置中的 `searchHybrid` 定义。\n\n---\n\n### 15.4 Elasticsearch 索引配置\n\n以下是为知识库检索场景设计的 Elasticsearch 索引模板和检索 DSL 配置,需放置在 `esmapper/knowledge.xml` 中。\n\n#### 15.4.1 索引结构与 Mapping\n\n```xml\n<property name=\"createKnowledgeChunksIndex\">\n <![CDATA[{\n \"settings\": {\n \"number_of_shards\": 1,\n \"number_of_replicas\": 0,\n \"analysis\": {\n \"filter\": {\n \"cn_stop\": {\n \"type\": \"stop\",\n \"stopwords\": [\"的\", \"了\", \"是\", \"在\", \"和\", \"或\", \"与\", \"对\", \"中\", \"也\", \"都\", \"就\", \"而\"]\n }\n },\n \"analyzer\": {\n \"cn_index_analyzer\": {\n \"type\": \"custom\",\n \"tokenizer\": \"ik_max_word\",\n \"filter\": [\"lowercase\", \"cn_stop\"]\n },\n \"cn_search_analyzer\": {\n \"type\": \"custom\",\n \"tokenizer\": \"ik_smart\",\n \"filter\": [\"lowercase\", \"cn_stop\"]\n }\n }\n }\n },\n \"mappings\": {\n \"properties\": {\n \"chunk_id\": { \"type\": \"keyword\" },\n \"document_id\": { \"type\": \"keyword\" },\n \"batch_id\": { \"type\": \"keyword\" },\n \"is_active\": { \"type\": \"boolean\" },\n \"doc_path\": { \"type\": \"keyword\" },\n \"anchor\": { \"type\": \"keyword\" },\n \"domain\": { \"type\": \"keyword\" },\n \"doc_type\": { \"type\": \"keyword\" },\n \"heading_path\": {\n \"type\": \"text\",\n \"analyzer\": \"cn_index_analyzer\",\n \"search_analyzer\": \"cn_search_analyzer\"\n },\n \"content\": {\n \"type\": \"text\",\n \"analyzer\": \"cn_index_analyzer\",\n \"search_analyzer\": \"cn_search_analyzer\",\n \"fields\": {\n \"en\": { \"type\": \"text\", \"analyzer\": \"english\" }\n }\n },\n \"embedding\": {\n \"type\": \"dense_vector\",\n \"dims\": 1024,\n \"index\": true,\n \"similarity\": \"cosine\"\n },\n \"created_at\": { \"type\": \"date\" }\n }\n }\n }]]>\n</property>\n```\n\n#### 15.4.2 KNN 向量检索 DSL\n\n```xml\n<property name=\"searchWithScore\">\n <![CDATA[{\n \"size\":#[size],\n \"knn\": {\n \"field\": \"embedding\",\n \"query_vector\": #[embedding,serialJson=true],\n \"k\": #[k],\n \"similarity\": #[similarity],\n \"num_candidates\": 100,\n \"boost\": 0.9\n }\n }]]>\n</property>\n```\n\n#### 15.4.3 BM25 文本检索 DSL\n\n```xml\n<property name=\"searchBM25\">\n <![CDATA[{\n \"size\": #[size],\n \"query\": {\n \"bool\": {\n \"must\": [\n {\n \"multi_match\": {\n \"query\": #[query],\n \"fields\": [\"content^3\", \"heading_path^2\"],\n \"type\": \"best_fields\"\n }\n }\n ],\n \"filter\": [\n { \"term\": { \"is_active\": #[is_active] } }\n ]\n }\n }\n }]]>\n</property>\n```\n\n#### 15.4.4 混合检索(KNN + BM25 + RRF)DSL\n\n```xml\n<property name=\"searchHybrid\">\n <![CDATA[{\n \"size\": #[size],\n \"query\": {\n \"bool\": {\n \"must\": [\n {\n \"multi_match\": {\n \"query\": #[query],\n \"fields\": [\"content^3\", \"heading_path^2\"],\n \"type\": \"best_fields\"\n }\n }\n ],\n \"filter\": [\n { \"term\": { \"is_active\": #[is_active] } }\n ]\n }\n },\n \"knn\": {\n \"field\": \"embedding\",\n \"query_vector\": #[embedding,serialJson=true],\n \"k\": #[k],\n \"similarity\": #[similarity],\n \"num_candidates\": 200,\n \"boost\": 0.5,\n \"filter\": [\n { \"term\": { \"is_active\": #[is_active] } }\n ]\n },\n \"rank\": {\n \"rrf\": {\n \"window_size\": 100,\n \"rank_constant\": 60\n }\n }\n }]]>\n</property>\n```\n\n#### 索引配置要点\n\n| 配置项 | 说明 |\n|--------|------|\n| `dense_vector` | `embedding` 字段类型,`dims: 1024` 对应 bge-m3 模型输出维度 |\n| `similarity: cosine` | 向量相似度算法,可选 `cosine`、`dot_product`、`l2_norm` |\n| `ik_max_word` / `ik_smart` | IK 分词器的索引模式和搜索模式 |\n| `cn_stop` | 自定义中文停用词过滤 |\n| `RRF` | Reciprocal Rank Fusion,Elasticsearch 原生多路检索融合算法 |\n\n> **注意**:使用 `searchHybrid` 需要 Elasticsearch 8.x 及以上版本,且索引的 `dense_vector` 字段必须设置 `index: true`。\n\n---\n\n## 十六 智能体 Trace 可观测性\n\nbboss-ai 内置了一套全链路、多维度的智能体 Trace 可观测性体系,覆盖 LLM 调用、工具执行、工作流编排等全部环节,支持内存和数据库、Clickhouse三中持久化方式,并可通过流式通道实时推送观测事件到前端。\n\n### **16.1 核心模型**\n\n| 类名 | 作用 |\n| --------------------------- | ------------------------------------------------------------ |\n| `TraceMessage` | Trace 消息载体,包含消息内容、起止时间、agentId、parentAgentId、traceId、metaData 等 |\n| `TokenMetrics` | Token 用量指标,包含 model、totalTokens、promptTokens、completionTokens、completionReasoningTokens、reasoningData、elapsed 等 |\n| `SessionMessage` | 会话消息类型体系,定义了 18 种消息类型常量,覆盖用户输入、LLM 输入输出、Embedding、Rerank、工具搜索、MCP 调用、Trace 等 |\n| `AgentMessageTypeConvertor` | 角色(role)到 messageType 的转换器,支持用户自定义扩展(101 起) |\n<a id=\"chapter16-2\"></a>\n\n### 16.2 消息类型对照表\n\n| 常量 | 值 | role 名称 | 说明 |\n| -------------------------------------- | ---- | --------------- | ------------------------------------------------------------ |\n| `MESSAGE_TYPE_ASSISTANT_MESSAGE` | 0 | assistant | 智能体辅助消息(如果是tool,无需在会话历史中呈现,因为需要通过toolsearch类型在历史中展示),大模型回复消息 |\n| `MESSAGE_TYPE_TOOL_MESSAGE_NAME` | 4 | tool | 智能体辅助消息:工具调用结果消息,需要提交给大模型 |\n| `MESSAGE_TYPE_AGENT_RESULTMESSAGE` | 1 | agentresult | 智能体输出结果 |\n| `MESSAGE_TYPE_USER_MESSAGE` | 2 | user | 用户输入消息(提交给模型)用户提问 |\n| `MESSAGE_TYPE_SYSTEM_MESSAGE` | 3 | system | 系统消息 |\n| `MESSAGE_TYPE_TRACE_MESSAGE` | 5 | trace | 智能体跟踪消息 |\n| `MESSAGE_TYPE_RAG_MESSAGE` | 6 | rag | RAG 知识库资料消息 |\n| `MESSAGE_TYPE_REFUSE_MESSAGE` | 7 | refuse | 拒答消息 |\n| `MESSAGE_TYPE_USER_INPUTMESSAGE` | 8 | userinput | 用户原始输入(含文件、图片描述) |\n| `MESSAGE_TYPE_LLM_INPUTMESSAGE` | 9 | llminput | 提交给 LLM 的完整请求报文 |\n| `MESSAGE_TYPE_LLM_OUTPUTMESSAGE` | 10 | llmoutput | LLM 返回的完整响应报文 |\n| `MESSAGE_TYPE_EMBEDDING_INPUTMESSAGE` | 11 | embeddinginput | Embedding 模型输入 |\n| `MESSAGE_TYPE_RERANK_INPUTMESSAGE` | 12 | rerankinput | Rerank 模型输入 |\n| `MESSAGE_TYPE_EMBEDDING_OUTPUTMESSAGE` | 13 | embeddingoutput | Embedding 模型输出 |\n| `MESSAGE_TYPE_RERANK_OUTPUTMESSAGE` | 14 | rerankoutput | Rerank 模型输出 |\n| `MESSAGE_TYPE_TOOLSEARCH_MESSAGE` | 15 | toolsearch | 工具搜索匹配消息(会话历史中需要展示) |\n| `MESSAGE_TYPE_MCPCALL_MESSAGE` | 16 | mcpcall | MCP 服务调用消息 |\n| `MESSAGE_TYPE_OTHER_MESSAGE` | 18 | other | 智能体工具服务调用消息:其他消息类型,定义的消息名称(role)没有设置对应的编码,返回18 |\n| `MESSAGE_TYPE_TOOLCALL_MESSAGE` | 17 | toolcall | 工具服务调用消息 |\n| MESSAGE_TYPE_SKILLCALL_MESSAGE | 19 | skillcall | 智能体Skill调用消息 |\n| MESSAGE_TYPE_HITL_MESSAGE | 20 | hitltask | 智能体人工介入消息:大模型提供输入内容,需要人工介入处理 |\n| MESSAGE_TYPE_HITL_HANDLE_MESSAGE | 21 | hitlhandle | 智能体人工介入处理消息:人工介入处理完成后,返回给大模型的消息和数据内容 |\n\n### **16.3 自动 Trace 采集**\n\n`AIAgentUtil` 在各类模型调用前后自动插入 Trace,无需业务代码介入:\n\n- `traceLLMInput()`:记录提交给模型的完整请求报文(LLM、Embedding、Rerank)\n- `traceLLMOutput()`:记录模型返回的完整响应报文\n- 覆盖场景:同步/流式聊天、图片生成、音频生成、视频任务提交、Embedding、Rerank\n\n### **16.4 流式 Token 计量**\n\n`BaseStreamDataBuilder` 在流式响应过程中通过 `computeTokens()` 累加各数据段的 Token 用量,最终在 `addChatWithToolCallSessionMessage()` 中将完整结果与 `TokenMetrics` 一并持久化。\n\n### **16.5 手动 Trace 记录**\n\n- **`AIAgent.recordTraceMessage()`**:智能体入口方法,自动补全 agentId 和 parentAgentId,写入主会话存储\n- **`AgentTraceHolder`**:基于 ThreadLocal 的 Trace 上下文持有者,支持在工具调用、异步执行等跨线程场景中安全记录 Trace;提供 `trace()` 和 `emitterServerEvent()` 方法\n- **`AIFlowNode.recordTraceMessage()` / `AIFlowNodeVoid.recordTraceMessage()`**:工作流普通节点支持手动记录 Trace,自动绑定 nodeId 和 parentAgentId\n\n### **16.6 工作流编排 Trace**\n\n工作流引擎在关键决策点自动记录 Trace:\n\n- **`AIRouterNodeBuilder`**:AI 路由节点记录路由选择结果(匹配成功/失败、重试次数)\n- **`AIKeywordsRouterNodeBuilder`**:关键词路由记录匹配到的智能体 ID 及描述\n- **自定义节点**:业务可通过 `recordTraceMessage()` 记录循环控制、条件判断等自定义轨迹(如循环次数、修复标记)\n\n### **16.7 实时流式推送**\n\n`ServerEvent` 支持 `TYPE_TRACE = 2` 类型,可将 Trace 信息通过 Flux/SSE 实时推送给前端。流式场景下,路由失败、重试等关键事件可即时反馈到用户界面。\n\n### **16.8 存储与持久化**\n\n```\nAgentSessionStore(接口)\n ├── AgentSessionStoreMemory(内存存储)\n └── AgentSessionStoreDB(数据库存储)\n └── agent_session_message 表\n```\n\n`AgentSessionStoreDB` 自动完成建表,核心字段包括:msgId、createTime、sessionId、parentAgentId、agentId、agentNodeType、subAgentIdBy、messageType、seqNo、message(JSON)、role、marks、metadata、requestId、tokenMetrics(JSON)、elapsed、traceId。\n\nTrace 消息与业务消息统一存储在同一张表中,通过 `messageType` 和 `role` 区分,便于按会话维度进行全链路回放和分析,例如:`messageType = 5` 和 `role = trace` 。具体消息类型映射关系,看上面的对照表。\n\n\n\n## 十七、技术支持\n\n- **技术交流群**:21220580、166471282\n\n- **参考文档**:https://doc.bbossgroups.com/#/mvc/chatstream\n\n- **多模态智能问答web demo**:https://gitee.com/bboss/bbootdemo\n 关键代码:\n\n 流式问答控制器 https://gitee.com/bboss/bbootdemo/blob/master/src/main/java/org/frameworkset/web/react/ReactorController.java\n\n 前端网页 https://gitee.com/bboss/bbootdemo/blob/master/WebRoot/chatBackuppressSession.html\n\n\n- **后端多模态演示案例**:https://gitee.com/bboss/bboss-ai\n\n 关键代码:https://gitee.com/bboss/bboss-ai/blob/main/bboss-ai/src/test/java/org/frameworkset/spi/ai/StreamTest.java"}}}} |