Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bbossgroups_bboss-elasticsearch.json
T

1 line
285 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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 模型平台,实现智能问答、图片识别/生成、语音识别/生成、视频识别/生成等功能。![img](images\\bboss-ai.png)\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 ![img](images\\workflow\\jobworkflow.png)![img](images\\workflow\\bbossgraph.png)\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![](images\\workflow\\bbossgraph.png)\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 模型平台,实现智能问答、图片识别/生成、语音识别/生成、视频识别/生成等功能。![img](images\\bboss-ai.png)\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 ![img](images\\workflow\\jobworkflow.png)![img](images\\workflow\\bbossgraph.png)\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![](images\\workflow\\bbossgraph.png)\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"}}}}