10 Commits

Author SHA1 Message Date
wangle
bf7b5eac72 fix:修复上下文消息构建顺序,确保AI正确理解对话上下文
消息顺序调整为:历史消息 → 知识库内容 → 当前用户消息

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 22:13:32 +08:00
wangle
d602b805bd docs:更新技术栈版本号并清理文档
- 更新后端架构版本为 Spring Boot 3.5.8 + Langchain4j
- 删除 rag-failures.md 和文件上传接口文档
- 重命名 mcp-api-spec.md 为 MCP工具模块接口文档.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 21:04:31 +08:00
ageerle
9cf18904bb Merge pull request #284 from MuSan-Li/main
添加可观测性功能
2026-04-07 10:04:05 +08:00
evo
2f39fa0f53 feat:调整可观测性监听器逻辑 2026-04-05 21:36:53 +08:00
evo
d2005cfa48 feat:调整可观测性监听器逻辑 2026-04-05 21:34:41 +08:00
evo
4e38f853f3 feat:修复登录校验 & 调整主启动类的kill port 逻辑 2026-04-02 10:07:26 +08:00
evo
3cfb185dde feat:增加可观测性监听器 调整思考输出监听日志 2026-04-01 23:11:54 +08:00
evo
ef99c540bb feat:增加可观测性的相关监听器 & 修复前端问答报错outputkey问题 2026-04-01 22:32:01 +08:00
ageerle
3071bfd0f9 Merge pull request #282 from Anush008/main
docs: Docker Compose setup for Qdrant
2026-03-28 20:33:25 +08:00
Anush008
7bb938c145 docs: Docker Compose setup for Qdrant 2026-03-28 13:37:53 +05:30
38 changed files with 1111 additions and 482 deletions

View File

@@ -32,7 +32,7 @@
| 模块 | 现有能力 | 模块 | 现有能力
|:----------:|--- |:----------:|---
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成 | **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate) + 文档解析 | **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态 | **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点 | **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型 | **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
@@ -62,8 +62,8 @@
## 🛠️ 技术架构 ## 🛠️ 技术架构
### 核心框架 ### 核心框架
- **后端架构**Spring Boot 4.0 + Spring ai 2.0 + Langchain4j - **后端架构**Spring Boot 3.5.8 + Langchain4j
- **数据存储**MySQL 8.0 + Redis + 向量数据库Milvus/Weaviate - **数据存储**MySQL 8.0 + Redis + 向量数据库Milvus/Weaviate/Qdrant
- **前端技术**Vue 3 + Vben Admin + element-plus-x - **前端技术**Vue 3 + Vben Admin + element-plus-x
- **安全认证**Sa-Token + JWT 双重保障 - **安全认证**Sa-Token + JWT 双重保障
@@ -189,12 +189,6 @@ docker-compose -f docker-compose-all.yaml restart [服务名]
**👉 [完整使用文档](https://doc.pandarobot.chat)** **👉 [完整使用文档](https://doc.pandarobot.chat)**
遇到知识库或 RAG 回答异常问题?
**👉 [RAG 回答异常排查手册](docs/troubleshooting/rag-failures.md)**
---
## 🤝 参与贡献 ## 🤝 参与贡献
我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪 我们热烈欢迎社区贡献!无论您是资深开发者还是初学者,都可以为项目贡献力量 💪

View File

@@ -35,7 +35,7 @@
| Module | Current Capabilities | | Module | Current Capabilities |
|:---:|---| |:---:|---|
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration | | **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate) + Document parsing | | **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem | | **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes | | **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
| **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models | | **Multi-Agent** | Agent framework based on Langchain4j, Supervisor mode orchestration, supports multiple decision models |
@@ -65,8 +65,8 @@
## 🛠️ Technical Architecture ## 🛠️ Technical Architecture
### Core Framework ### Core Framework
- **Backend**: Spring Boot 4.0 + Spring AI 2.0 + Langchain4j - **Backend**: Spring Boot 3.5.8 + Langchain4j
- **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate) - **Data Storage**: MySQL 8.0 + Redis + Vector Databases (Milvus/Weaviate/Qdrant)
- **Frontend**: Vue 3 + Vben Admin + element-plus-x - **Frontend**: Vue 3 + Vben Admin + element-plus-x
- **Security**: Sa-Token + JWT dual-layer security - **Security**: Sa-Token + JWT dual-layer security
@@ -192,12 +192,6 @@ Want to learn more about installation, deployment, configuration, and secondary
**👉 [Complete Documentation](https://doc.pandarobot.chat)** **👉 [Complete Documentation](https://doc.pandarobot.chat)**
Experiencing issues with knowledge base or RAG responses?
**👉 [RAG Troubleshooting Guide](docs/troubleshooting/rag-failures.md)**
---
## 🤝 Contributing ## 🤝 Contributing
We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪 We warmly welcome community contributions! Whether you are a seasoned developer or just getting started, you can contribute to the project 💪

View File

@@ -0,0 +1,12 @@
---
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- 6333:6333
- 6334:6334
volumes:
- qdrant_data:/qdrant/storage
volumes:
qdrant_data:
...

View File

@@ -1,352 +0,0 @@
<a id="top"></a>
# RAG 常见故障排查16 问题清单)
当知识库已经接入,系统也能正常回答,但结果仍然出现命中错误、引用旧内容、推理漂移、跨轮次失忆,或部署后表面可用但实际异常时,最常见的问题不是“模型不行”,而是**不同层的故障被混在一起处理**。
这份页面不重新发明一套新方法。
它直接使用一份固定的 **16 问题清单** 作为排查主轴,让你先把问题标到正确的 **No.X**,再决定下一步查哪里、改哪里,而不是一次性乱改检索、模型、切块、会话和部署配置。
这份清单的核心目的只有一个:
**先把问题放进正确的故障域,再做修复。**
快速导航:
[这页怎么用](#how-to-use) | [标签说明](#legend) | [常见症状入口](#symptoms) | [16 问题清单](#map16) | [按层排查](#by-layer) |
---
<a id="how-to-use"></a>
## 一、这页怎么用
这不是一篇“从头到尾照着做”的传统教程。
它更像一张固定的 RAG 故障地图,作用是先帮助你**判断故障属于哪一种类型**。
建议按下面顺序使用:
### 1. 先看现象,不要先改配置
先回答两个问题:
1. 你看到的故障,最像哪一种症状
2. 这个故障更像发生在输入检索层、推理层、状态层,还是部署层
在还没判断层级之前,不要先一起改这些东西:
- 检索条数
- 切块大小
- 会话配置
- 模型参数
- 部署顺序
- 依赖服务
如果先全部一起动,问题通常只会更难定位。
### 2. 先给问题打上 No.X 标签
这份页面最重要的动作,不是“立刻修好”,而是先做一件小事:
**给当前问题贴上最接近的 No.X。**
例如:
- 检索结果看起来相似,但其实答非所问,先看 `No.1``No.5`
- 切块是对的,但结论还是错,先看 `No.2`
- 系统回答很自信,但没有根据,先看 `No.4`
- 刚部署完就炸,先看 `No.14``No.16`
### 3. 一次只排一个故障域
同一个表面现象,背后可能是不同层的问题。
例如“答案不对”既可能是:
- `No.1` 检索漂移
- `No.2` 理解塌陷
- `No.4` 自信乱答
- `No.8` 根本看不到错误路径
所以这张表的用法不是“多选全改”,而是:
**先挑最接近的一项,优先验证这一项是否成立。**
[返回顶部](#top) | [下一节:标签说明](#legend)
---
<a id="legend"></a>
## 二、标签说明
这份 16 问题清单本身已经带有层级 / 标签结构。
这些标签不是装饰,而是用来帮助你快速判断故障发生在哪一层。
### 1. 层级标签
- `[IN]`:输入与检索
输入、切块、召回、语义匹配、可见性问题
- `[RE]`:推理与规划
理解、推理、归纳、逻辑链、抽象处理问题
- `[ST]`:状态与上下文
会话、记忆、上下文连续性、多代理状态问题
- `[OP]`:基础设施与部署
启动顺序、依赖就绪、部署锁死、预部署状态问题
### 2. `{OBS}` 标签
`{OBS}` 的项,通常都和“**你是否看得见问题是怎么坏掉的**”有关。
它们往往不是单纯回答错误,而是:
- 错误路径不可见
- 漂移过程不可见
- 状态熔化过程不可见
- 多代理覆盖过程不可见
所以一旦你发现“我知道结果错,但我根本看不到它是怎么错的”,通常就已经很接近 `{OBS}` 类问题了。
### 3. 为什么要保留这些标签
因为同样叫“答错了”,实际含义完全不同。
例如:
- `[IN]` 的答错,常常是**拿错材料**
- `[RE]` 的答错,常常是**拿对材料但理解错**
- `[ST]` 的答错,常常是**前文断掉、状态漂移**
- `[OP]` 的答错,常常是**系统根本没在完整状态下运行**
如果不先分层,就会掉进典型的 RAG 地狱:
表面在改答案,实际上在盲修。
[返回顶部](#top) | [下一节:常见症状入口](#symptoms)
---
<a id="symptoms"></a>
## 三、常见症状入口
如果你现在还不知道该从哪一项开始,就先从症状入口反查。
### 1. 检索返回了错误内容,或看起来相关但其实不回答问题
这类问题最常见的是:
“有命中,但命中的不是该用的内容。”
优先看:
- [No.1](#no1) `幻觉与切块漂移`
- [No.5](#no5) `语义 ≠ 向量嵌入`
- [No.8](#no8) `调试是一个黑箱`
### 2. 切块本身是对的,但最终答案还是错的
这类问题不是简单没检索到,而是后面那层坏了。
优先看:
- [No.2](#no2) `解释塌陷`
- [No.4](#no4) `虚张声势 / 过度自信`
- [No.6](#no6) `逻辑塌陷与恢复`
### 3. 多步任务一开始正常,后面越来越偏
这类问题通常不是单点错误,而是中途漂移或熔化。
优先看:
- [No.3](#no3) `长推理链`
- [No.6](#no6) `逻辑塌陷与恢复`
- [No.9](#no9) `熵塌陷`
### 4. 多轮对话后开始失忆,跨轮次接不上
这类问题一般已经进入状态层。
优先看:
- [No.7](#no7) `跨会话记忆断裂`
- [No.9](#no9) `熵塌陷`
- [No.13](#no13) `多代理混乱`
### 5. 遇到抽象、逻辑、规则、符号关系就崩
这类问题通常不是检索空,而是推理结构扛不住。
优先看:
- [No.11](#no11) `符号塌陷`
- [No.12](#no12) `哲学递归`
### 6. 你根本不知道错在哪一层,只知道结果不对
这类问题先不要乱调参数。
先解决“不可见”的问题。
优先看:
- [No.8](#no8) `调试是一个黑箱`
### 7. 刚部署完最容易炸,首轮调用异常,重启后偶尔恢复
这类问题通常不在答案逻辑,而在部署状态。
优先看:
- [No.14](#no14) `引导启动顺序`
- [No.15](#no15) `部署死锁`
- [No.16](#no16) `预部署塌陷`
[返回顶部](#top) | [下一节16 问题清单](#map16)
---
<a id="map16"></a>
## 四、16 问题清单(固定主表)
下面这 16 项按固定顺序使用。
不要先重组,不要先混类,先判断最接近哪一个 **No.X**
| # | 问题域(含层级/标签) | 会坏在哪里 |
|---|---|---|
| <a id="no1"></a> 1 | `[IN] 幻觉与切块漂移 {OBS}` | 检索返回错误/无关内容 |
| <a id="no2"></a> 2 | `[RE] 解释塌陷` | 切块是对的,逻辑是错的 |
| <a id="no3"></a> 3 | `[RE] 长推理链 {OBS}` | 在多步任务中逐步漂移 |
| <a id="no4"></a> 4 | `[RE] 虚张声势 / 过度自信` | 自信但没有根据的回答 |
| <a id="no5"></a> 5 | `[IN] 语义 ≠ 向量嵌入 {OBS}` | 余弦匹配 ≠ 真实语义 |
| <a id="no6"></a> 6 | `[RE] 逻辑塌陷与恢复 {OBS}` | 走入死胡同,需要受控重置 |
| <a id="no7"></a> 7 | `[ST] 跨会话记忆断裂` | 线索丢失,没有连续性 |
| <a id="no8"></a> 8 | `[IN] 调试是一个黑箱 {OBS}` | 看不到故障路径 |
| <a id="no9"></a> 9 | `[ST] 熵塌陷` | 注意力熔化,输出失去连贯性 |
| <a id="no10"></a> 10 | `[RE] 创造力冻结` | 平直、字面化输出 |
| <a id="no11"></a> 11 | `[RE] 符号塌陷` | 抽象/逻辑性提示词失效 |
| <a id="no12"></a> 12 | `[RE] 哲学递归` | 自我引用循环、悖论陷阱 |
| <a id="no13"></a> 13 | `[ST] 多代理混乱 {OBS}` | 代理互相覆盖或使逻辑错位 |
| <a id="no14"></a> 14 | `[OP] 引导启动顺序` | 依赖未就绪时服务先启动 |
| <a id="no15"></a> 15 | `[OP] 部署死锁` | 基础设施中的循环等待 |
| <a id="no16"></a> 16 | `[OP] 预部署塌陷 {OBS}` | 首次调用时版本错配 / 缺少密钥 |
这张表是主表。
如果你时间很少,只做一件事也行:
**先从这 16 项里选出最接近的一项。**
[返回顶部](#top) | [下一节:按层排查](#by-layer)
---
<a id="by-layer"></a>
## 五、按层排查:不要改错层
这一节不重写 16 项,只是告诉你:
当你已经选到某个 No.X 时,第一眼应该优先查哪一层。
### A. `[IN]` 层:先确认你拿到的是不是对的材料
对应编号:
- [No.1](#no1)
- [No.5](#no5)
- [No.8](#no8)
这层最常见的误判是:
“我以为系统理解错了,其实它一开始就拿错了东西。”
如果你命中了弱相关片段、表面相似文本、错误切块,后面推理再强也没用。
所以 `[IN]` 层优先看的是:
1. 原始召回内容到底是什么
2. 命中的片段是否只是“相似”,而不是“正确”
3. 你是否能看到检索过程,还是整个过程像黑箱
这层如果没先排好,后面的推理诊断通常会失真。
### B. `[RE]` 层:材料可能是对的,但系统用错了
对应编号:
- [No.2](#no2)
- [No.3](#no3)
- [No.4](#no4)
- [No.6](#no6)
- [No.10](#no10)
- [No.11](#no11)
- [No.12](#no12)
这层最常见的误判是:
“我以为是检索坏了,其实是后面理解、归纳、逻辑链坏了。”
例如:
- 切块是对的,但结论错了 → 常见是 `No.2`
- 多步任务中途开始偏 → 常见是 `No.3`
- 回答很笃定,但完全站不住 → 常见是 `No.4`
- 遇到抽象规则就崩 → 常见是 `No.11`
- 陷入循环解释 → 常见是 `No.12`
如果 `[IN]` 层已经基本没问题,答案还是不对,就应该优先回到 `[RE]` 层判断是哪一种塌陷。
### C. `[ST]` 层:单轮正常,不代表状态层正常
对应编号:
- [No.7](#no7)
- [No.9](#no9)
- [No.13](#no13)
这层最常见的误判是:
“单轮看起来还行,所以我以为系统没问题。”
其实很多 RAG 地狱不是单轮错误,而是:
- 多轮之后前文断掉
- 上下文越来越乱
- 多角色、多代理之间互相覆盖
如果你发现:
- 第一轮没事,后面越来越歪
- 切换角色后前面的约束消失
- 多个步骤之间状态彼此污染
那就不要再只盯着检索条数了,应该直接回到 `[ST]` 层看 `No.7 / No.9 / No.13`
### D. `[OP]` 层:别把部署问题误诊成回答问题
对应编号:
- [No.14](#no14)
- [No.15](#no15)
- [No.16](#no16)
这层最常见的误判是:
“答案不稳定,所以我先去调模型或检索。”
但如果系统根本没有在完整状态下启动,所有上层表现都会像鬼打墙。
尤其是这些情况:
- 依赖还没就绪,服务先起了 → `No.14`
- 多个组件互相等待,长期半可用 → `No.15`
- 首次调用就因为版本、密钥、环境没对齐而塌陷 → `No.16`
只要你看到“刚部署最容易出事”“首轮异常最严重”“重启后暂时恢复”,就要优先怀疑 `[OP]` 层,而不是先改提示词或参数。
[返回顶部](#top) |
---
<a id="issue-report"></a>
## 六、快速返回
[返回顶部](#top) | [这页怎么用](#how-to-use) | [标签说明](#legend) | [常见症状入口](#symptoms) | [16 问题清单](#map16) | [按层排查](#by-layer)

View File

@@ -1,42 +0,0 @@
## 接口信息
**接口路径**: `POST /resource/oss/upload`
**请求类型**: `multipart/form-data`
**权限要求**: `system:oss:upload`
**业务类型**: [INSERT]
### 接口描述
上传OSS对象存储接口用于将文件上传到对象存储服务。
### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
| ---- | ------------- | ---- | ------ |
| file | MultipartFile | 是 | 要上传的文件 |
### 请求头
- `Content-Type`: `multipart/form-data`
### 返回值
返回 `R<SysOssUploadVo>` 类型,包含以下字段:
| 字段名 | 类型 | 说明 |
| -------- | ------ | ------- |
| url | String | 文件访问URL |
| fileName | String | 原始文件名 |
| ossId | String | 文件ID |
### 响应示例
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"url": "fileid://xxx",
"fileName": "example.jpg",
"ossId": "123"
}
}
```
### 异常情况
- 当上传文件为空时,返回错误信息:"上传文件不能为空"

View File

@@ -4,6 +4,9 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup; import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
/** /**
* 启动程序 * 启动程序
* *
@@ -13,10 +16,66 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
public class RuoYiAIApplication { public class RuoYiAIApplication {
public static void main(String[] args) { public static void main(String[] args) {
// killPortProcess(6039);
SpringApplication application = new SpringApplication(RuoYiAIApplication.class); SpringApplication application = new SpringApplication(RuoYiAIApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048)); application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args); application.run(args);
System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)"); System.out.println("(♥◠‿◠)ノ゙ RuoYi-AI启动成功 ლ(´ڡ`ლ)");
}
/**
* 检查并终止占用指定端口的进程
*
* @param port 端口号
*/
private static void killPortProcess(int port) {
try {
if (!isPortInUse(port)) {
return;
}
System.out.println("端口 " + port + " 已被占用,正在查找并终止进程...");
ProcessBuilder pb = new ProcessBuilder("netstat", "-ano");
Process process = pb.start();
java.io.BufferedReader reader = new java.io.BufferedReader(
new java.io.InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains(":" + port + " ") && line.contains("LISTENING")) {
String[] parts = line.trim().split("\\s+");
String pid = parts[parts.length - 1];
System.out.println("找到占用端口 " + port + " 的进程 PID: " + pid + ",正在终止...");
ProcessBuilder killPb = new ProcessBuilder("taskkill", "/F", "/PID", pid);
Process killProcess = killPb.start();
int exitCode = killProcess.waitFor();
if (exitCode == 0) {
System.out.println("进程 " + pid + " 已成功终止");
} else {
System.out.println("终止进程 " + pid + " 失败exitCode: " + exitCode);
}
break;
}
}
// 等待一小段时间确保端口释放
Thread.sleep(500);
} catch (Exception e) {
System.out.println("检查/终止端口进程时发生异常: " + e.getMessage());
}
}
/**
* 检查端口是否被占用
*/
private static boolean isPortInUse(int port) {
try (ServerSocket socket = new ServerSocket()) {
socket.bind(new InetSocketAddress(port));
return false;
} catch (Exception e) {
return true;
}
} }
} }

View File

@@ -6,7 +6,7 @@ import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V; import dev.langchain4j.service.V;
public interface ChartGenerationAgent extends Agent { public interface ChartGenerationAgent {
@SystemMessage(""" @SystemMessage("""
You are a chart generation specialist. Your only task is to generate Apache ECharts You are a chart generation specialist. Your only task is to generate Apache ECharts

View File

@@ -11,7 +11,7 @@ import dev.langchain4j.service.V;
* and returning relevant data and analysis results. * and returning relevant data and analysis results.
* *
*/ */
public interface SqlAgent extends Agent { public interface SqlAgent {
@SystemMessage(""" @SystemMessage("""
This agent is designed for MySQL 5.7 This agent is designed for MySQL 5.7

View File

@@ -10,7 +10,7 @@ import dev.langchain4j.service.V;
* A web search assistant that answers natural language questions by searching the internet * A web search assistant that answers natural language questions by searching the internet
* and returning relevant information from web pages. * and returning relevant information from web pages.
*/ */
public interface WebSearchAgent extends Agent { public interface WebSearchAgent {
@SystemMessage(""" @SystemMessage("""
You are a web search assistant. Answer questions by searching and retrieving web content. You are a web search assistant. Answer questions by searching and retrieving web content.

View File

@@ -43,7 +43,7 @@ public class ExecuteSqlQueryTool implements BuiltinToolProvider {
@Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user") @Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user")
public String executeSql(String sql) { public String executeSql(String sql) {
// 2. 手动推入数据源上下文 // 2. 手动推入数据源上下文
DynamicDataSourceContextHolder.push("agent"); // DynamicDataSourceContextHolder.push("agent");
if (sql == null || sql.trim().isEmpty()) { if (sql == null || sql.trim().isEmpty()) {
return "Error: SQL query cannot be empty"; return "Error: SQL query cannot be empty";
} }

View File

@@ -2,8 +2,9 @@ package org.ruoyi.factory;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.service.chat.IChatModelService;
import org.ruoyi.observability.EmbeddingModelListenerProvider;
import org.ruoyi.service.embed.BaseEmbedModelService; import org.ruoyi.service.embed.BaseEmbedModelService;
import org.ruoyi.service.embed.MultiModalEmbedModelService; import org.ruoyi.service.embed.MultiModalEmbedModelService;
import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoSuchBeanDefinitionException;
@@ -27,6 +28,7 @@ public class EmbeddingModelFactory {
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final IChatModelService chatModelService; private final IChatModelService chatModelService;
private final EmbeddingModelListenerProvider embeddingModelListenerProvider;
// 模型缓存使用ConcurrentHashMap保证线程安全 // 模型缓存使用ConcurrentHashMap保证线程安全
private final Map<String, BaseEmbedModelService> modelCache = new ConcurrentHashMap<>(); private final Map<String, BaseEmbedModelService> modelCache = new ConcurrentHashMap<>();
@@ -109,6 +111,8 @@ public class EmbeddingModelFactory {
BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class); BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class);
// 配置模型参数 // 配置模型参数
model.configure(config); model.configure(config);
// 增加嵌入模型监听器
model.addListeners(embeddingModelListenerProvider.getEmbeddingModelListeners());
log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderCode(), config.getId()); log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderCode(), config.getId());
return model; return model;
} catch (NoSuchBeanDefinitionException e) { } catch (NoSuchBeanDefinitionException e) {

View File

@@ -0,0 +1,41 @@
package org.ruoyi.observability;
import cn.hutool.core.collection.CollUtil;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
import lombok.Getter;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* LangChain4j 监听器共享提供者。
* <p>
* 供所有 {@link dev.langchain4j.model.chat.StreamingChatModel} 构建器使用,
* 将可观测性监听器注入到模型实例中。
*
* @author evo
*/
@Component
@Getter
@Lazy
public class ChatModelListenerProvider {
private final List<ChatModelListener> chatModelListeners;
private final List<EmbeddingModelListener> embeddingModelListeners;
public ChatModelListenerProvider(@Nullable List<ChatModelListener> chatModelListeners,
@Nullable List<EmbeddingModelListener> embeddingModelListeners) {
if (CollUtil.isEmpty(chatModelListeners)) {
chatModelListeners = Collections.emptyList();
}
if (CollUtil.isEmpty(embeddingModelListeners)) {
embeddingModelListeners = Collections.emptyList();
}
this.chatModelListeners = chatModelListeners;
this.embeddingModelListeners = embeddingModelListeners;
}
}

View File

@@ -0,0 +1,34 @@
package org.ruoyi.observability;
import cn.hutool.core.collection.CollUtil;
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
import lombok.Getter;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* EmbeddingModel 监听器共享提供者。
* <p>
* 供所有 {@link dev.langchain4j.model.embedding.EmbeddingModel} 构建器使用,
* 将可观测性监听器注入到模型实例中。
*
* @author evo
*/
@Component
@Getter
@Lazy
public class EmbeddingModelListenerProvider {
private final List<EmbeddingModelListener> embeddingModelListeners;
public EmbeddingModelListenerProvider(@Nullable List<EmbeddingModelListener> embeddingModelListeners) {
if (CollUtil.isEmpty(embeddingModelListeners)) {
embeddingModelListeners = Collections.emptyList();
}
this.embeddingModelListeners = embeddingModelListeners;
}
}

View File

@@ -0,0 +1,129 @@
package org.ruoyi.observability;
import dev.langchain4j.Experimental;
import dev.langchain4j.mcp.client.McpClientListener;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
import dev.langchain4j.observability.api.AiServiceListenerRegistrar;
import dev.langchain4j.observability.api.listener.*;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* LangChain4j 可观测性配置类。
* <p>
* 负责注册所有 langchain4j 的监听器:
* <ul>
* <li>{@link AiServiceListener} - AI服务级别的事件监听器通过 AiServiceListenerRegistrar 注册)</li>
* <li>{@link ChatModelListener} - ChatModel 级别的监听器(注入到模型构建器)</li>
* <li>{@link EmbeddingModelListener} - EmbeddingModel 级别的监听器(注入到模型构建器)</li>
* </ul>
*
* @author evo
*/
@Configuration
@RequiredArgsConstructor
@Slf4j
public class LangChain4jObservabilityConfig {
private final AiServiceListenerRegistrar registrar = AiServiceListenerRegistrar.newInstance();
/**
* 注册 AI 服务级别的事件监听器
*/
@PostConstruct
public void registerAiServiceListeners() {
log.info("正在注册 LangChain4j AI Service 事件监听器...");
registrar.register(
new MyAiServiceStartedListener(),
new MyAiServiceRequestIssuedListener(),
new MyAiServiceResponseReceivedListener(),
new MyAiServiceCompletedListener(),
new MyAiServiceErrorListener(),
new MyInputGuardrailExecutedListener(),
new MyOutputGuardrailExecutedListener(),
new MyToolExecutedEventListener()
);
log.info("LangChain4j AI Service 事件监听器注册完成");
}
// ==================== AI Service 监听器 Beans ====================
@Bean
public AiServiceStartedListener aiServiceStartedListener() {
return new MyAiServiceStartedListener();
}
@Bean
public AiServiceRequestIssuedListener aiServiceRequestIssuedListener() {
return new MyAiServiceRequestIssuedListener();
}
@Bean
public AiServiceResponseReceivedListener aiServiceResponseReceivedListener() {
return new MyAiServiceResponseReceivedListener();
}
@Bean
public AiServiceCompletedListener aiServiceCompletedListener() {
return new MyAiServiceCompletedListener();
}
@Bean
public AiServiceErrorListener aiServiceErrorListener() {
return new MyAiServiceErrorListener();
}
@Bean
public InputGuardrailExecutedListener inputGuardrailExecutedListener() {
return new MyInputGuardrailExecutedListener();
}
@Bean
public OutputGuardrailExecutedListener outputGuardrailExecutedListener() {
return new MyOutputGuardrailExecutedListener();
}
@Bean
public ToolExecutedEventListener toolExecutedEventListener() {
return new MyToolExecutedEventListener();
}
// ==================== ChatModel 监听器 ====================
@Bean
public ChatModelListener chatModelListener() {
return new MyChatModelListener();
}
@Bean
public List<ChatModelListener> chatModelListeners() {
return List.of(new MyChatModelListener());
}
// ==================== EmbeddingModel 监听器 ====================
@Bean
@Experimental
public EmbeddingModelListener embeddingModelListener() {
return new MyEmbeddingModelListener();
}
@Bean
@Experimental
public List<EmbeddingModelListener> embeddingModelListeners() {
return List.of(new MyEmbeddingModelListener());
}
// ==================== MCP Client 监听器 ====================
@Bean
public McpClientListener mcpClientListener() {
return new MyMcpClientListener();
}
}

View File

@@ -0,0 +1,145 @@
package org.ruoyi.observability;
import dev.langchain4j.agentic.observability.AgentInvocationError;
import dev.langchain4j.agentic.observability.AgentRequest;
import dev.langchain4j.agentic.observability.AgentResponse;
import dev.langchain4j.agentic.planner.AgentInstance;
import dev.langchain4j.agentic.scope.AgenticScope;
import dev.langchain4j.service.tool.BeforeToolExecution;
import dev.langchain4j.service.tool.ToolExecution;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* 自定义的 AgentListener 的监听器。
* 监听 Agent 相关的所有可观测性事件,包括:
* <ul>
* <li>Agent 调用前/后的生命周期事件</li>
* <li>Agent 执行错误事件</li>
* <li>AgenticScope 的创建/销毁事件</li>
* <li>工具执行前/后的生命周期事件</li>
* </ul>
*
* @author evo
*/
@Slf4j
public class MyAgentListener implements dev.langchain4j.agentic.observability.AgentListener {
/** 最终捕获到的思考结果(主 Agent 完成后写入,供外部获取) */
private final AtomicReference<String> sharedOutputRef = new AtomicReference<>();
public String getCapturedResult() {
return sharedOutputRef.get();
}
// ==================== Agent 调用生命周期 ====================
@Override
public void beforeAgentInvocation(AgentRequest agentRequest) {
AgentInstance agent = agentRequest.agent();
AgenticScope scope = agentRequest.agenticScope();
Map<String, Object> inputs = agentRequest.inputs();
log.info("【Agent调用前】Agent名称: {}", agent.name());
log.info("【Agent调用前】Agent ID: {}", agent.agentId());
log.info("【Agent调用前】Agent类型: {}", agent.type().getName());
log.info("【Agent调用前】Agent描述: {}", agent.description());
log.info("【Agent调用前】Planner类型: {}", agent.plannerType());
log.info("【Agent调用前】输出类型: {}", agent.outputType());
log.info("【Agent调用前】输出Key: {}", agent.outputKey());
log.info("【Agent调用前】是否为异步: {}", agent.async());
log.info("【Agent调用前】是否为叶子节点: {}", agent.leaf());
log.info("【Agent调用前】Agent参数列表:");
for (var arg : agent.arguments()) {
log.info(" - 参数名: {}, 类型: {}, 默认值: {}",
arg.name(), arg.rawType().getName(), arg.defaultValue());
}
log.info("【Agent调用前】Agent输入参数: {}", inputs);
log.info("【Agent调用前】AgenticScope memoryId: {}", scope.memoryId());
log.info("【Agent调用前】AgenticScope当前状态: {}", scope.state());
log.info("【Agent调用前】Agent调用历史记录数: {}", scope.agentInvocations().size());
// 打印嵌套的子Agent信息
if (!agent.subagents().isEmpty()) {
log.info("【Agent调用前】子Agent列表:");
for (AgentInstance sub : agent.subagents()) {
log.info(" - 子Agent: {} ({})", sub.name(), sub.type().getName());
}
}
// 打印父Agent信息
if (agent.parent() != null) {
log.info("【Agent调用前】父Agent: {}", agent.parent().name());
}
}
@Override
public void afterAgentInvocation(AgentResponse agentResponse) {
AgentInstance agent = agentResponse.agent();
Map<String, Object> inputs = agentResponse.inputs();
Object output = agentResponse.output();
String outputStr = output != null ? output.toString() : "";
log.info("【Agent调用后】Agent名称: {}", agent.name());
log.info("【Agent调用后】Agent ID: {}", agent.agentId());
log.info("【Agent调用后】Agent输入参数: {}", inputs);
log.info("【Agent调用后】Agent输出结果: {}", output);
log.info("【Agent调用后】是否为叶子节点: {}", agent.leaf());
// 捕获主 Agent 的最终输出,供外部获取
if ("invoke".equals(agent.agentId()) && !outputStr.isEmpty()) {
sharedOutputRef.set(outputStr);
log.info("【Agent调用后】已捕获主Agent输出: {}", outputStr);
}
}
@Override
public void onAgentInvocationError(AgentInvocationError error) {
AgentInstance agent = error.agent();
Map<String, Object> inputs = error.inputs();
Throwable throwable = error.error();
log.error("【Agent执行错误】Agent名称: {}", agent.name());
log.error("【Agent执行错误】Agent ID: {}", agent.agentId());
log.error("【Agent执行错误】Agent类型: {}", agent.type().getName());
log.error("【Agent执行错误】Agent输入参数: {}", inputs);
log.error("【Agent执行错误】错误类型: {}", throwable.getClass().getName());
log.error("【Agent执行错误】错误信息: {}", throwable.getMessage(), throwable);
}
// ==================== AgenticScope 生命周期 ====================
@Override
public void afterAgenticScopeCreated(AgenticScope agenticScope) {
log.info("【AgenticScope已创建】memoryId: {}", agenticScope.memoryId());
log.info("【AgenticScope已创建】初始状态: {}", agenticScope.state());
}
@Override
public void beforeAgenticScopeDestroyed(AgenticScope agenticScope) {
log.info("【AgenticScope即将销毁】memoryId: {}", agenticScope.memoryId());
log.info("【AgenticScope即将销毁】最终状态: {}", agenticScope.state());
log.info("【AgenticScope即将销毁】总调用次数: {}", agenticScope.agentInvocations().size());
}
// ==================== 工具执行生命周期 ====================
@Override
public void beforeToolExecution(BeforeToolExecution beforeToolExecution) {
var toolRequest = beforeToolExecution.request();
log.info("【工具执行前】工具请求ID: {}", toolRequest.id());
log.info("【工具执行前】工具名称: {}", toolRequest.name());
log.info("【工具执行前】工具参数: {}", toolRequest.arguments());
}
@Override
public void afterToolExecution(ToolExecution toolExecution) {
var toolRequest = toolExecution.request();
log.info("【工具执行后】工具请求ID: {}", toolRequest.id());
log.info("【工具执行后】工具名称: {}", toolRequest.name());
log.info("【工具执行后】工具执行结果: {}", toolExecution.result());
log.info("【工具执行后】工具执行是否失败: {}", toolExecution.hasFailed());
}
}

View File

@@ -0,0 +1,41 @@
package org.ruoyi.observability;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.observability.api.event.AiServiceCompletedEvent;
import dev.langchain4j.observability.api.listener.AiServiceCompletedListener;
import lombok.extern.slf4j.Slf4j;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
* 自定义的 AiServiceCompletedEvent 的监听器。
* 它表示在 AI 服务调用完成时发生的事件。
*
* @author evo
*/
@Slf4j
public class MyAiServiceCompletedListener implements AiServiceCompletedListener {
@Override
public void onEvent(AiServiceCompletedEvent event) {
InvocationContext invocationContext = event.invocationContext();
Optional<Object> result = event.result();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
List<Object> aiServiceMethodArgs = invocationContext.methodArguments();
Object chatMemoryId = invocationContext.chatMemoryId();
Instant eventTimestamp = invocationContext.timestamp();
log.info("【AI服务完成】调用唯一标识符: {}", invocationId);
log.info("【AI服务完成】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【AI服务完成】调用的方法名: {}", aiServiceMethodName);
log.info("【AI服务完成】AI服务方法参数: {}", aiServiceMethodArgs);
log.info("【AI服务完成】聊天记忆ID: {}", chatMemoryId);
log.info("【AI服务完成】调用发生的时间: {}", eventTimestamp);
log.info("【AI服务完成】调用结果: {}", result);
}
}

View File

@@ -0,0 +1,33 @@
package org.ruoyi.observability;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.observability.api.event.AiServiceErrorEvent;
import dev.langchain4j.observability.api.listener.AiServiceErrorListener;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID;
/**
* 自定义的 AiServiceErrorEvent 的监听器。
* 它表示在 AI 服务调用失败时发生的事件。
*
* @author evo
*/
@Slf4j
public class MyAiServiceErrorListener implements AiServiceErrorListener {
@Override
public void onEvent(AiServiceErrorEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
Throwable error = event.error();
log.error("【AI服务错误】调用唯一标识符: {}", invocationId);
log.error("【AI服务错误】AI服务接口名: {}", aiServiceInterfaceName);
log.error("【AI服务错误】调用的方法名: {}", aiServiceMethodName);
log.error("【AI服务错误】错误类型: {}", error.getClass().getName());
log.error("【AI服务错误】错误信息: {}", error.getMessage(), error);
}
}

View File

@@ -0,0 +1,33 @@
package org.ruoyi.observability;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.observability.api.event.AiServiceRequestIssuedEvent;
import dev.langchain4j.observability.api.listener.AiServiceRequestIssuedListener;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID;
/**
* 自定义的 AiServiceRequestIssuedEvent 的监听器。
* 它表示在向 LLM 发送请求之前发生的事件。
*
* @author evo
*/
@Slf4j
public class MyAiServiceRequestIssuedListener implements AiServiceRequestIssuedListener {
@Override
public void onEvent(AiServiceRequestIssuedEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
ChatRequest request = event.request();
log.info("【请求已发出】调用唯一标识符: {}", invocationId);
log.info("【请求已发出】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【请求已发出】调用的方法名: {}", aiServiceMethodName);
log.info("【请求已发出】发送给LLM的请求: {}", request);
}
}

View File

@@ -0,0 +1,37 @@
package org.ruoyi.observability;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.observability.api.event.AiServiceResponseReceivedEvent;
import dev.langchain4j.observability.api.listener.AiServiceResponseReceivedListener;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID;
/**
* 自定义的 AiServiceResponseReceivedEvent 的监听器。
* 它表示在从 LLM 接收到响应时发生的事件。
* 在涉及工具或 guardrail 的单个 AI 服务调用期间,可能会被调用多次。
*
* @author evo
*/
@Slf4j
public class MyAiServiceResponseReceivedListener implements AiServiceResponseReceivedListener {
@Override
public void onEvent(AiServiceResponseReceivedEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
ChatRequest request = event.request();
ChatResponse response = event.response();
log.info("【响应已接收】调用唯一标识符: {}", invocationId);
log.info("【响应已接收】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【响应已接收】调用的方法名: {}", aiServiceMethodName);
log.info("【响应已接收】发送给LLM的请求: {}", request);
log.info("【响应已接收】从LLM收到的响应: {}", response);
}
}

View File

@@ -0,0 +1,38 @@
package org.ruoyi.observability;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.observability.api.event.AiServiceStartedEvent;
import dev.langchain4j.observability.api.listener.AiServiceStartedListener;
import lombok.extern.slf4j.Slf4j;
import java.util.Optional;
import java.util.UUID;
/**
* 自定义的 AiServiceStartedEvent 的监听器。
* 它表示在 AI 服务调用开始时发生的事件。
*
* @author evo
*/
@Slf4j
public class MyAiServiceStartedListener implements AiServiceStartedListener {
@Override
public void onEvent(AiServiceStartedEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
Optional<SystemMessage> systemMessage = event.systemMessage();
UserMessage userMessage = event.userMessage();
log.info("【AI服务启动】调用唯一标识符: {}", invocationId);
log.info("【AI服务启动】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【AI服务启动】调用的方法名: {}", aiServiceMethodName);
log.info("【AI服务启动】系统消息: {}", systemMessage.orElse(null));
log.info("【AI服务启动】用户消息: {}", userMessage);
}
}

View File

@@ -0,0 +1,43 @@
package org.ruoyi.observability;
import dev.langchain4j.model.chat.listener.ChatModelErrorContext;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.chat.listener.ChatModelRequestContext;
import dev.langchain4j.model.chat.listener.ChatModelResponseContext;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.response.ChatResponse;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义的 ChatModelListener 的监听器。
* 它监听 ChatModel 的请求、响应和错误事件。
*
* @author evo
*/
@Slf4j
public class MyChatModelListener implements ChatModelListener {
@Override
public void onRequest(ChatModelRequestContext requestContext) {
ChatRequest request = requestContext.chatRequest();
log.info("【ChatModel请求】发送给模型的请求: {}", request);
log.info("【ChatModel请求】模型提供商: {}", requestContext.modelProvider());
}
@Override
public void onResponse(ChatModelResponseContext responseContext) {
ChatRequest request = responseContext.chatRequest();
ChatResponse response = responseContext.chatResponse();
log.info("【ChatModel响应】原始请求: {}", request);
log.info("【ChatModel响应】收到的响应: {}", response);
log.info("【ChatModel响应】模型提供商: {}", responseContext.modelProvider());
}
@Override
public void onError(ChatModelErrorContext errorContext) {
log.error("【ChatModel错误】错误类型: {}", errorContext.error().getClass().getName());
log.error("【ChatModel错误】错误信息: {}", errorContext.error().getMessage());
log.error("【ChatModel错误】原始请求: {}", errorContext.chatRequest());
log.error("【ChatModel错误】模型提供商: {}", errorContext.modelProvider());
}
}

View File

@@ -0,0 +1,47 @@
package org.ruoyi.observability;
import dev.langchain4j.Experimental;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.model.embedding.listener.EmbeddingModelErrorContext;
import dev.langchain4j.model.embedding.listener.EmbeddingModelListener;
import dev.langchain4j.model.embedding.listener.EmbeddingModelRequestContext;
import dev.langchain4j.model.embedding.listener.EmbeddingModelResponseContext;
import dev.langchain4j.model.output.Response;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 自定义的 EmbeddingModelListener 的监听器。
* 它监听 EmbeddingModel 的请求、响应和错误事件。
*
* @author evo
*/
@Slf4j
@Experimental
public class MyEmbeddingModelListener implements EmbeddingModelListener {
@Override
public void onRequest(EmbeddingModelRequestContext requestContext) {
log.info("【EmbeddingModel请求】输入文本段落数量: {}", requestContext.textSegments().size());
log.info("【EmbeddingModel请求】嵌入模型: {}", requestContext.embeddingModel());
}
@Override
public void onResponse(EmbeddingModelResponseContext responseContext) {
Response<List<Embedding>> response = responseContext.response();
List<Embedding> embeddings = response.content();
log.info("【EmbeddingModel响应】嵌入向量数量: {}", embeddings.size());
log.info("【EmbeddingModel响应】嵌入维度: {}", embeddings.isEmpty() ? 0 : embeddings.get(0).dimension());
log.info("【EmbeddingModel响应】嵌入模型: {}", responseContext.embeddingModel());
log.info("【EmbeddingModel响应】输入文本段落: {}", responseContext.textSegments());
}
@Override
public void onError(EmbeddingModelErrorContext errorContext) {
log.error("【EmbeddingModel错误】错误类型: {}", errorContext.error().getClass().getName());
log.error("【EmbeddingModel错误】错误信息: {}", errorContext.error().getMessage());
log.error("【EmbeddingModel错误】输入文本段落数量: {}", errorContext.textSegments().size());
log.error("【EmbeddingModel错误】嵌入模型: {}", errorContext.embeddingModel());
}
}

View File

@@ -0,0 +1,45 @@
package org.ruoyi.observability;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailRequest;
import dev.langchain4j.guardrail.InputGuardrailResult;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.observability.api.event.InputGuardrailExecutedEvent;
import dev.langchain4j.observability.api.listener.InputGuardrailExecutedListener;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.UUID;
/**
* 自定义的 InputGuardrailExecutedEvent 的监听器。
* 它表示在输入 guardrail 验证执行时发生的事件。
*
* @author evo
*/
@Slf4j
public class MyInputGuardrailExecutedListener implements InputGuardrailExecutedListener {
@Override
public void onEvent(InputGuardrailExecutedEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
InputGuardrailRequest request = event.request();
InputGuardrailResult result = event.result();
Class<InputGuardrail> guardrailClass = event.guardrailClass();
Duration duration = event.duration();
UserMessage rewrittenUserMessage = event.rewrittenUserMessage();
log.info("【输入Guardrail已执行】调用唯一标识符: {}", invocationId);
log.info("【输入Guardrail已执行】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【输入Guardrail已执行】调用的方法名: {}", aiServiceMethodName);
log.info("【输入Guardrail已执行】Guardrail类名: {}", guardrailClass.getName());
log.info("【输入Guardrail已执行】输入Guardrail请求: {}", request);
log.info("【输入Guardrail已执行】输入Guardrail结果: {}", result);
log.info("【输入Guardrail已执行】重写后的用户消息: {}", rewrittenUserMessage);
log.info("【输入Guardrail已执行】执行耗时: {}ms", duration.toMillis());
}
}

View File

@@ -0,0 +1,136 @@
package org.ruoyi.observability;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.mcp.client.McpCallContext;
import dev.langchain4j.mcp.client.McpClientListener;
import dev.langchain4j.mcp.client.McpGetPromptResult;
import dev.langchain4j.mcp.client.McpReadResourceResult;
import dev.langchain4j.mcp.protocol.McpClientMessage;
import dev.langchain4j.service.tool.ToolExecutionResult;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 自定义的 McpClientListener 的监听器。
* 监听 MCP 客户端相关的所有可观测性事件,包括:
* <ul>
* <li>MCP 工具执行的开始/成功/错误事件</li>
* <li>MCP 资源读取的开始/成功/错误事件</li>
* <li>MCP 提示词获取的开始/成功/错误事件</li>
* </ul>
*
* @author evo
*/
@Slf4j
public class MyMcpClientListener implements McpClientListener {
// ==================== 工具执行 ====================
@Override
public void beforeExecuteTool(McpCallContext context) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.info("【MCP工具执行前】调用唯一标识符: {}", invocationContext.invocationId());
log.info("【MCP工具执行前】MCP消息ID: {}", message.getId());
log.info("【MCP工具执行前】MCP方法: {}", message.method);
}
@Override
public void afterExecuteTool(McpCallContext context, ToolExecutionResult result, Map<String, Object> rawResult) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.info("【MCP工具执行后】调用唯一标识符: {}", invocationContext.invocationId());
log.info("【MCP工具执行后】MCP消息ID: {}", message.getId());
log.info("【MCP工具执行后】MCP方法: {}", message.method);
log.info("【MCP工具执行后】工具执行结果: {}", result);
log.info("【MCP工具执行后】原始结果: {}", rawResult);
}
@Override
public void onExecuteToolError(McpCallContext context, Throwable error) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.error("【MCP工具执行错误】调用唯一标识符: {}", invocationContext.invocationId());
log.error("【MCP工具执行错误】MCP消息ID: {}", message.getId());
log.error("【MCP工具执行错误】MCP方法: {}", message.method);
log.error("【MCP工具执行错误】错误类型: {}", error.getClass().getName());
log.error("【MCP工具执行错误】错误信息: {}", error.getMessage(), error);
}
// ==================== 资源读取 ====================
@Override
public void beforeResourceGet(McpCallContext context) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.info("【MCP资源读取前】调用唯一标识符: {}", invocationContext.invocationId());
log.info("【MCP资源读取前】MCP消息ID: {}", message.getId());
log.info("【MCP资源读取前】MCP方法: {}", message.method);
}
@Override
public void afterResourceGet(McpCallContext context, McpReadResourceResult result, Map<String, Object> rawResult) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.info("【MCP资源读取后】调用唯一标识符: {}", invocationContext.invocationId());
log.info("【MCP资源读取后】MCP消息ID: {}", message.getId());
log.info("【MCP资源读取后】MCP方法: {}", message.method);
log.info("【MCP资源读取后】资源内容数量: {}", result.contents() != null ? result.contents().size() : 0);
log.info("【MCP资源读取后】原始结果: {}", rawResult);
}
@Override
public void onResourceGetError(McpCallContext context, Throwable error) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.error("【MCP资源读取错误】调用唯一标识符: {}", invocationContext.invocationId());
log.error("【MCP资源读取错误】MCP消息ID: {}", message.getId());
log.error("【MCP资源读取错误】MCP方法: {}", message.method);
log.error("【MCP资源读取错误】错误类型: {}", error.getClass().getName());
log.error("【MCP资源读取错误】错误信息: {}", error.getMessage(), error);
}
// ==================== 提示词获取 ====================
@Override
public void beforePromptGet(McpCallContext context) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.info("【MCP提示词获取前】调用唯一标识符: {}", invocationContext.invocationId());
log.info("【MCP提示词获取前】MCP消息ID: {}", message.getId());
log.info("【MCP提示词获取前】MCP方法: {}", message.method);
}
@Override
public void afterPromptGet(McpCallContext context, McpGetPromptResult result, Map<String, Object> rawResult) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.info("【MCP提示词获取后】调用唯一标识符: {}", invocationContext.invocationId());
log.info("【MCP提示词获取后】MCP消息ID: {}", message.getId());
log.info("【MCP提示词获取后】MCP方法: {}", message.method);
log.info("【MCP提示词获取后】提示词描述: {}", result.description());
log.info("【MCP提示词获取后】提示词消息数量: {}", result.messages() != null ? result.messages().size() : 0);
log.info("【MCP提示词获取后】原始结果: {}", rawResult);
}
@Override
public void onPromptGetError(McpCallContext context, Throwable error) {
InvocationContext invocationContext = context.invocationContext();
McpClientMessage message = context.message();
log.error("【MCP提示词获取错误】调用唯一标识符: {}", invocationContext.invocationId());
log.error("【MCP提示词获取错误】MCP消息ID: {}", message.getId());
log.error("【MCP提示词获取错误】MCP方法: {}", message.method);
log.error("【MCP提示词获取错误】错误类型: {}", error.getClass().getName());
log.error("【MCP提示词获取错误】错误信息: {}", error.getMessage(), error);
}
}

View File

@@ -0,0 +1,42 @@
package org.ruoyi.observability;
import dev.langchain4j.guardrail.OutputGuardrail;
import dev.langchain4j.guardrail.OutputGuardrailRequest;
import dev.langchain4j.guardrail.OutputGuardrailResult;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.observability.api.event.OutputGuardrailExecutedEvent;
import dev.langchain4j.observability.api.listener.OutputGuardrailExecutedListener;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.util.UUID;
/**
* 自定义的 OutputGuardrailExecutedEvent 的监听器。
* 它表示在输出 guardrail 验证执行时发生的事件。
*
* @author evo
*/
@Slf4j
public class MyOutputGuardrailExecutedListener implements OutputGuardrailExecutedListener {
@Override
public void onEvent(OutputGuardrailExecutedEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
OutputGuardrailRequest request = event.request();
OutputGuardrailResult result = event.result();
Class<OutputGuardrail> guardrailClass = event.guardrailClass();
Duration duration = event.duration();
log.info("【输出Guardrail已执行】调用唯一标识符: {}", invocationId);
log.info("【输出Guardrail已执行】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【输出Guardrail已执行】调用的方法名: {}", aiServiceMethodName);
log.info("【输出Guardrail已执行】Guardrail类名: {}", guardrailClass.getName());
log.info("【输出Guardrail已执行】输出Guardrail请求: {}", request);
log.info("【输出Guardrail已执行】输出Guardrail结果: {}", result);
log.info("【输出Guardrail已执行】执行耗时: {}ms", duration.toMillis());
}
}

View File

@@ -0,0 +1,38 @@
package org.ruoyi.observability;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.invocation.InvocationContext;
import dev.langchain4j.observability.api.event.ToolExecutedEvent;
import dev.langchain4j.observability.api.listener.ToolExecutedEventListener;
import lombok.extern.slf4j.Slf4j;
import java.util.UUID;
/**
* 自定义的 ToolExecutedEvent 的监听器。
* 它表示在工具执行完成后发生的事件。
* 在单个 AI 服务调用期间,可能会被调用多次。
*
* @author evo
*/
@Slf4j
public class MyToolExecutedEventListener implements ToolExecutedEventListener {
@Override
public void onEvent(ToolExecutedEvent event) {
InvocationContext invocationContext = event.invocationContext();
UUID invocationId = invocationContext.invocationId();
String aiServiceInterfaceName = invocationContext.interfaceName();
String aiServiceMethodName = invocationContext.methodName();
ToolExecutionRequest request = event.request();
String resultText = event.resultText();
log.info("【工具已执行】调用唯一标识符: {}", invocationId);
log.info("【工具已执行】AI服务接口名: {}", aiServiceInterfaceName);
log.info("【工具已执行】调用的方法名: {}", aiServiceMethodName);
log.info("【工具已执行】工具执行请求 ID: {}", request.id());
log.info("【工具已执行】工具名称: {}", request.name());
log.info("【工具已执行】工具参数: {}", request.arguments());
log.info("【工具已执行】工具执行结果: {}", resultText);
}
}

View File

@@ -1,11 +1,13 @@
package org.ruoyi.service.chat.impl; package org.ruoyi.service.chat.impl;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.StrUtil;
import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.AgenticServices;
import dev.langchain4j.agentic.supervisor.SupervisorAgent; import dev.langchain4j.agentic.supervisor.SupervisorAgent;
import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy;
import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.*; import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.mcp.McpToolProvider; import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient; import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient; import dev.langchain4j.mcp.client.McpClient;
@@ -43,6 +45,9 @@ import org.ruoyi.domain.bo.vector.QueryVectorBo;
import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo; import org.ruoyi.domain.vo.knowledge.KnowledgeInfoVo;
import org.ruoyi.factory.ChatServiceFactory; import org.ruoyi.factory.ChatServiceFactory;
import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.mcp.service.core.ToolProviderFactory;
import org.ruoyi.observability.MyAgentListener;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.observability.MyMcpClientListener;
import org.ruoyi.service.chat.AbstractChatService; import org.ruoyi.service.chat.AbstractChatService;
import org.ruoyi.service.chat.IChatMessageService; import org.ruoyi.service.chat.IChatMessageService;
import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore;
@@ -120,7 +125,7 @@ public class ChatServiceFacade implements IChatService {
List<ChatMessage> contextMessages = buildContextMessages(chatRequest); List<ChatMessage> contextMessages = buildContextMessages(chatRequest);
// 3. 处理特殊聊天模式(工作流、人机交互恢复、思考模式) // 3. 处理特殊聊天模式(工作流、人机交互恢复、思考模式)
SseEmitter specialResult = handleSpecialChatModes(chatRequest, contextMessages, chatModelVo, emitter); SseEmitter specialResult = handleSpecialChatModes(chatRequest, contextMessages, chatModelVo, emitter, userId, tokenValue);
if (specialResult != null) { if (specialResult != null) {
return specialResult; return specialResult;
} }
@@ -149,10 +154,13 @@ public class ChatServiceFacade implements IChatService {
* @param contextMessages 上下文消息列表(可能被修改) * @param contextMessages 上下文消息列表(可能被修改)
* @param chatModelVo 聊天模型配置 * @param chatModelVo 聊天模型配置
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param userId 用户ID
* @param tokenValue 会话令牌
* @return 如果需要提前返回则返回SseEmitter否则返回null * @return 如果需要提前返回则返回SseEmitter否则返回null
*/ */
private SseEmitter handleSpecialChatModes(ChatRequest chatRequest, List<ChatMessage> contextMessages, private SseEmitter handleSpecialChatModes(ChatRequest chatRequest, List<ChatMessage> contextMessages,
ChatModelVo chatModelVo, SseEmitter emitter) { ChatModelVo chatModelVo, SseEmitter emitter,
Long userId, String tokenValue) {
// 处理工作流对话 // 处理工作流对话
if (chatRequest.getEnableWorkFlow()) { if (chatRequest.getEnableWorkFlow()) {
log.info("处理工作流对话,会话: {}", chatRequest.getSessionId()); log.info("处理工作流对话,会话: {}", chatRequest.getSessionId());
@@ -191,7 +199,7 @@ public class ChatServiceFacade implements IChatService {
// 处理思考模式 // 处理思考模式
if (chatRequest.getEnableThinking()) { if (chatRequest.getEnableThinking()) {
handleThinkingMode(chatRequest, contextMessages, chatModelVo); handleThinkingMode(chatRequest, contextMessages, chatModelVo, userId);
} }
return null; return null;
@@ -200,11 +208,13 @@ public class ChatServiceFacade implements IChatService {
/** /**
* 处理思考模式 * 处理思考模式
* *
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @param contextMessages 上下文消息列表 * @param contextMessages 上下文消息列表
* @param chatModelVo 聊天模型配置 * @param chatModelVo 聊天模型配置
* @param userId 用户ID
*/ */
private void handleThinkingMode(ChatRequest chatRequest, List<ChatMessage> contextMessages, ChatModelVo chatModelVo) { private void handleThinkingMode(ChatRequest chatRequest, List<ChatMessage> contextMessages,
ChatModelVo chatModelVo, Long userId) {
// 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器 // 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器
McpTransport transport = new StdioMcpTransport.Builder() McpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "bing-cn-mcp")) .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", "bing-cn-mcp"))
@@ -213,6 +223,7 @@ public class ChatServiceFacade implements IChatService {
McpClient mcpClient = new DefaultMcpClient.Builder() McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport) .transport(transport)
.listener(new MyMcpClientListener())
.build(); .build();
ToolProvider toolProvider = McpToolProvider.builder() ToolProvider toolProvider = McpToolProvider.builder()
@@ -227,6 +238,7 @@ public class ChatServiceFacade implements IChatService {
McpClient mcpClient1 = new DefaultMcpClient.Builder() McpClient mcpClient1 = new DefaultMcpClient.Builder()
.transport(transport1) .transport(transport1)
.listener(new MyMcpClientListener())
.build(); .build();
ToolProvider toolProvider1 = McpToolProvider.builder() ToolProvider toolProvider1 = McpToolProvider.builder()
@@ -237,34 +249,40 @@ public class ChatServiceFacade implements IChatService {
OpenAiChatModel plannerModel = OpenAiChatModel.builder() OpenAiChatModel plannerModel = OpenAiChatModel.builder()
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.listeners(List.of(new MyChatModelListener()))
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.build(); .build();
// 构建各Agent // 构建各Agent
SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class) SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class)
.chatModel(plannerModel) .chatModel(plannerModel)
.listener(new MyAgentListener())
.tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool()) .tools(new QueryAllTablesTool(), new QueryTableSchemaTool(), new ExecuteSqlQueryTool())
.build(); .build();
WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class) WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class)
.chatModel(plannerModel) .chatModel(plannerModel)
.listener(new MyAgentListener())
.toolProvider(toolProvider) .toolProvider(toolProvider)
.build(); .build();
ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class) ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class)
.chatModel(plannerModel) .chatModel(plannerModel)
.listener(new MyAgentListener())
.toolProvider(toolProvider1) .toolProvider(toolProvider1)
.build(); .build();
// 构建监督者Agent // 构建监督者Agent
SupervisorAgent supervisor = AgenticServices.supervisorBuilder() SupervisorAgent supervisor = AgenticServices.supervisorBuilder()
.chatModel(plannerModel) .chatModel(plannerModel)
.subAgents(sqlAgent, chartGenerationAgent) .listener(new MyAgentListener())
.subAgents(sqlAgent, searchAgent, chartGenerationAgent)
.responseStrategy(SupervisorResponseStrategy.LAST) .responseStrategy(SupervisorResponseStrategy.LAST)
.build(); .build();
// 调用 supervisor
String invoke = supervisor.invoke(chatRequest.getContent()); String invoke = supervisor.invoke(chatRequest.getContent());
contextMessages.add(AiMessage.from(invoke)); log.info("supervisor.invoke() 返回: {}", invoke);
} }
/** /**
@@ -341,44 +359,16 @@ public class ChatServiceFacade implements IChatService {
/** /**
* 构建上下文消息列表 * 构建上下文消息列表
* 消息顺序:历史消息 → 当前用户消息(确保 AI 正确理解对话上下文)
* *
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @return 上下文消息列表 * @return 上下文消息列表
*/ */
private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) { private List<ChatMessage> buildContextMessages(ChatRequest chatRequest) {
List<ChatMessage> messages = new ArrayList<>(); List<ChatMessage> messages = new ArrayList<>();
// 构建用户消息
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
messages.add(userMessage);
// 从向量库查询相关历史消息 // 从数据库查询历史对话消息(放在前面)
if (chatRequest.getKnowledgeId() != null) {
// 查询知识库信息
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
if (knowledgeInfoVo == null) {
log.warn("知识库信息不存在kid: {}", chatRequest.getKnowledgeId());
return messages;
}
// 查询向量模型配置信息
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
if (chatModel == null) {
log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel());
return messages;
}
// 构建向量查询参数
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
// 获取向量查询结果
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
for (String prompt : nearestList) {
// 知识库内容作为系统上下文添加
messages.add( new AiMessage(prompt));
}
}
// 从数据库查询历史对话消息
if (chatRequest.getSessionId() != null) { if (chatRequest.getSessionId() != null) {
MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId()); MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId());
if (memory != null) { if (memory != null) {
@@ -390,6 +380,40 @@ public class ChatServiceFacade implements IChatService {
} }
} }
// 从向量库查询相关历史消息(知识库内容作为上下文)
if (chatRequest.getKnowledgeId() != null) {
// 查询知识库信息
KnowledgeInfoVo knowledgeInfoVo = knowledgeInfoService.queryById(Long.valueOf(chatRequest.getKnowledgeId()));
if (knowledgeInfoVo == null) {
log.warn("知识库信息不存在kid: {}", chatRequest.getKnowledgeId());
// 继续添加当前用户消息
messages.add(UserMessage.userMessage(chatRequest.getContent()));
return messages;
}
// 查询向量模型配置信息
ChatModelVo chatModel = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModel());
if (chatModel == null) {
log.warn("向量模型配置不存在,模型名称: {}", knowledgeInfoVo.getEmbeddingModel());
messages.add(UserMessage.userMessage(chatRequest.getContent()));
return messages;
}
// 构建向量查询参数
QueryVectorBo queryVectorBo = buildQueryVectorBo(chatRequest, knowledgeInfoVo, chatModel);
// 获取向量查询结果(知识库内容作为系统上下文,放在历史消息之后)
List<String> nearestList = vectorStoreService.getQueryVector(queryVectorBo);
for (String prompt : nearestList) {
// 知识库内容作为系统上下文添加
messages.add(new AiMessage(prompt));
}
}
// 构建当前用户消息(放在最后)
UserMessage userMessage = UserMessage.userMessage(chatRequest.getContent());
messages.add(userMessage);
return messages; return messages;
} }

View File

@@ -1,22 +1,30 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.ChatModelListenerProvider;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService; import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* @Author: xiaoen * Deepseek服务调用
* @Description: deepseek 服务调用 *
* @Date: Created in 19:12 2026/3/17 * @author xiaoen
* @date 2026/3/17
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class DeepseekServiceImpl implements AbstractChatService { public class DeepseekServiceImpl implements AbstractChatService {
@Override @Override
@@ -25,6 +33,7 @@ public class DeepseekServiceImpl implements AbstractChatService {
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking()) .returnThinking(chatRequest.getEnableThinking())
.build(); .build();
} }

View File

@@ -1,13 +1,19 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.ChatModelListenerProvider;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* OllamaAI服务调用 * OllamaAI服务调用
@@ -17,13 +23,17 @@ import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class OllamaServiceImpl implements AbstractChatService { public class OllamaServiceImpl implements AbstractChatService {
private final ChatModelListenerProvider listenerProvider;
@Override @Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
return OllamaStreamingChatModel.builder() return OllamaStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.build(); .build();
} }

View File

@@ -3,13 +3,18 @@ package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.ChatModelListenerProvider;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService; import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* OPENAI服务调用 * OPENAI服务调用
@@ -19,14 +24,16 @@ import org.springframework.stereotype.Service;
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class OpenAIServiceImpl implements AbstractChatService { public class OpenAIServiceImpl implements AbstractChatService {
@Override @Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
return OpenAiStreamingChatModel.builder() return OpenAiStreamingChatModel.builder()
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking()) .returnThinking(chatRequest.getEnableThinking())
.build(); .build();
} }

View File

@@ -1,22 +1,28 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService; import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* OPENAI服务调用 * PPIO服务调用
* *
* @author ageerle@163.com * @author ageerle@163.com
* @date 2025/12/13 * @date 2025/12/13
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class PPIOServiceImpl implements AbstractChatService { public class PPIOServiceImpl implements AbstractChatService {
@Override @Override
@@ -25,6 +31,7 @@ public class PPIOServiceImpl implements AbstractChatService {
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.returnThinking(chatRequest.getEnableThinking()) .returnThinking(chatRequest.getEnableThinking())
.build(); .build();
} }

View File

@@ -1,14 +1,20 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.ChatModelListenerProvider;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService; import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* qianWenAI服务调用 * qianWenAI服务调用
@@ -18,14 +24,17 @@ import org.springframework.stereotype.Service;
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class QianWenChatServiceImpl implements AbstractChatService { public class QianWenChatServiceImpl implements AbstractChatService {
// 添加文档解析的前缀字段 private final ChatModelListenerProvider listenerProvider;
@Override @Override
public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { public StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) {
return QwenStreamingChatModel.builder() return QwenStreamingChatModel.builder()
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.build(); .build();
} }

View File

@@ -1,14 +1,19 @@
package org.ruoyi.service.chat.impl.provider; package org.ruoyi.service.chat.impl.provider;
import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel; import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.StreamingChatModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.enums.ChatModeType; import org.ruoyi.enums.ChatModeType;
import org.ruoyi.observability.MyChatModelListener;
import org.ruoyi.service.chat.AbstractChatService; import org.ruoyi.service.chat.AbstractChatService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
/** /**
* 智谱AI服务调用 * 智谱AI服务调用
@@ -18,6 +23,7 @@ import org.springframework.stereotype.Service;
*/ */
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor
public class ZhiPuChatServiceImpl implements AbstractChatService { public class ZhiPuChatServiceImpl implements AbstractChatService {
@Override @Override
@@ -25,6 +31,7 @@ public class ZhiPuChatServiceImpl implements AbstractChatService {
return ZhipuAiStreamingChatModel.builder() return ZhipuAiStreamingChatModel.builder()
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.model(chatModelVo.getModelName()) .model(chatModelVo.getModelName())
.listeners(List.of(new MyChatModelListener()))
.build(); .build();
} }

View File

@@ -4,10 +4,11 @@ package org.ruoyi.service.embed.impl;
import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel; import dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;
import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.output.Response; import dev.langchain4j.model.output.Response;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.springframework.stereotype.Component;
import org.ruoyi.enums.ModalityType; import org.ruoyi.enums.ModalityType;
import org.springframework.stereotype.Component;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -20,7 +21,6 @@ import java.util.Set;
@Component("alibailian") @Component("alibailian")
public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider { public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
private ChatModelVo chatModelVo; private ChatModelVo chatModelVo;
@Override @Override
@@ -35,12 +35,13 @@ public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider {
@Override @Override
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) { public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
return QwenEmbeddingModel.builder() EmbeddingModel model = QwenEmbeddingModel.builder()
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.dimension(chatModelVo.getModelDimension()) .dimension(chatModelVo.getModelDimension())
.build() .build();
.embedAll(textSegments);
return model.embedAll(textSegments);
} }
} }

View File

@@ -2,6 +2,7 @@ package org.ruoyi.service.embed.impl;
import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel; import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import dev.langchain4j.model.output.Response; import dev.langchain4j.model.output.Response;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
@@ -34,10 +35,11 @@ public class OllamaEmbeddingProvider implements BaseEmbedModelService {
// ollama不能设置embedding维度使用milvus时请注意创建向量表时需要先设定维度大小 // ollama不能设置embedding维度使用milvus时请注意创建向量表时需要先设定维度大小
@Override @Override
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) { public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
return OllamaEmbeddingModel.builder() EmbeddingModel model = OllamaEmbeddingModel.builder()
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.build() .build();
.embedAll(textSegments);
return model.embedAll(textSegments);
} }
} }

View File

@@ -2,6 +2,7 @@ package org.ruoyi.service.embed.impl;
import dev.langchain4j.data.embedding.Embedding; import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment; import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel; import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import dev.langchain4j.model.output.Response; import dev.langchain4j.model.output.Response;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
@@ -33,12 +34,13 @@ public class OpenAiEmbeddingProvider implements BaseEmbedModelService {
@Override @Override
public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) { public Response<List<Embedding>> embedAll(List<TextSegment> textSegments) {
return OpenAiEmbeddingModel.builder() EmbeddingModel model = OpenAiEmbeddingModel.builder()
.baseUrl(chatModelVo.getApiHost()) .baseUrl(chatModelVo.getApiHost())
.apiKey(chatModelVo.getApiKey()) .apiKey(chatModelVo.getApiKey())
.modelName(chatModelVo.getModelName()) .modelName(chatModelVo.getModelName())
.dimensions(chatModelVo.getModelDimension()) .dimensions(chatModelVo.getModelDimension())
.build() .build();
.embedAll(textSegments);
return model.embedAll(textSegments);
} }
} }