Browse Source

求移除未使用的 PRODUCT_EXTRACT(商品信息抽取)类型配置

dev-mcp
wanghanlin 4 weeks ago
parent
commit
8e586a8c35
  1. 5
      CLAUDE.md
  2. 1
      DEPLOY.md
  3. 4
      README.md
  4. 57
      src/main/java/com/wok/supportbot/app/ProductInfoApp.java
  5. 2
      src/main/java/com/wok/supportbot/config/ChatModelFactory.java
  6. 1
      src/main/java/com/wok/supportbot/config/ModelConfigLoader.java
  7. 17
      src/main/java/com/wok/supportbot/controller/AiController.java
  8. 2
      src/main/java/com/wok/supportbot/entity/AiModelConfig.java
  9. 14
      src/main/java/com/wok/supportbot/entity/ProductInfo.java
  10. 4
      src/main/java/com/wok/supportbot/service/AiModelConfigService.java
  11. 5
      src/main/resources/static/components/ModelConfigManager.js
  12. 32
      src/test/java/com/wok/supportbot/SupportBotApplicationTests.java

5
CLAUDE.md

@ -39,11 +39,10 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
### Spring AI 集成模式 ### Spring AI 集成模式
- **ChatClient Builder**: 所有对话通过 `ChatClient.builder(chatModelFactory.getChatModel("CHAT"))` 构建,ChatModel 由 `ChatModelFactory` 按 DB 活跃配置动态创建 - **ChatClient Builder**: 所有对话通过 `ChatClient.builder(chatModelFactory.getChatModel("CHAT"))` 构建,ChatModel 由 `ChatModelFactory` 按 DB 活跃配置动态创建
- **Advisor 链**: `MessageChatMemoryAdvisor`(记忆) → `MyLoggerAdvisor`(日志) → `QuestionAnswerAdvisor`(RAG) - **Advisor 链**: `MessageChatMemoryAdvisor`(记忆) → `MyLoggerAdvisor`(日志) → `QuestionAnswerAdvisor`(RAG)
- **结构化输出**: `ProductInfoApp` 使用 `.entity(ProductInfo.class)` 提取结构化数据
- **SSE 流式**: 三种实现 — Flux\<String\>、Flux\<ServerSentEvent\>、SseEmitter - **SSE 流式**: 三种实现 — Flux\<String\>、Flux\<ServerSentEvent\>、SseEmitter
### ChatMemory 持久化 ### ChatMemory 持久化
当前使用 `DatabaseChatMemory`(PostgreSQL 持久化),`FileBasedChatMemory`(Kryo 序列化)已注释掉。`ProductInfoApp` 单独使用 `InMemoryChatMemory`
当前使用 `DatabaseChatMemory`(PostgreSQL 持久化),`FileBasedChatMemory`(Kryo 序列化)已注释掉。
### RAG 双模式 ### RAG 双模式
1. **QuestionAnswerAdvisor 模式**(生产使用): 预检索优化 + `QuestionAnswerAdvisor` 1. **QuestionAnswerAdvisor 模式**(生产使用): 预检索优化 + `QuestionAnswerAdvisor`
@ -69,7 +68,7 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支
- **数据库自动初始化**: `DatabaseInitConfig` 在启动时检查并创建 `knowledge_category`/`knowledge_document`/`ai_model_config` 等表,对已存在的 `knowledge_document` 表会自动补加 `content_hash` 列。注意 `knowledge-base.sql` 脚本为早期版本,缺少此列,实际以 `DatabaseInitConfig` 为准 - **数据库自动初始化**: `DatabaseInitConfig` 在启动时检查并创建 `knowledge_category`/`knowledge_document`/`ai_model_config` 等表,对已存在的 `knowledge_document` 表会自动补加 `content_hash` 列。注意 `knowledge-base.sql` 脚本为早期版本,缺少此列,实际以 `DatabaseInitConfig` 为准
### 模型配置管理 ### 模型配置管理
- **ai_model_config 表**: 存储大模型配置,支持多套配置按 App 类型(CHAT / PRODUCT_EXTRACT / EMBEDDING / RAG_REWRITE)独立管理。**所有模型参数(名称、温度、最大Token、API Key、Base URL 等)全部由此表管理,不再依赖 application.yml**
- **ai_model_config 表**: 存储大模型配置,支持多套配置按 App 类型(CHAT / EMBEDDING / RAG_REWRITE)独立管理。**所有模型参数(名称、温度、最大Token、API Key、Base URL 等)全部由此表管理,不再依赖 application.yml**
- **激活互斥**: 同一 App 类型只能有一个 `is_active=true` 的配置,激活操作由 Service 层 `@Transactional` 保证 - **激活互斥**: 同一 App 类型只能有一个 `is_active=true` 的配置,激活操作由 Service 层 `@Transactional` 保证
- **启动 seed**: 首次启动时使用硬编码默认值写入 DB(`qwen-turbo` / `text-embedding-v2`),用户可在前端修改。**DashScope 自动配置已禁用**(`spring.ai.dashscope.enabled=false`),api-key 可选——yml 与 DB 均未配置 api-key 不影响启动,实际调用 AI 功能前在前端「AI 大模型配置管理」页面配置即可 - **启动 seed**: 首次启动时使用硬编码默认值写入 DB(`qwen-turbo` / `text-embedding-v2`),用户可在前端修改。**DashScope 自动配置已禁用**(`spring.ai.dashscope.enabled=false`),api-key 可选——yml 与 DB 均未配置 api-key 不影响启动,实际调用 AI 功能前在前端「AI 大模型配置管理」页面配置即可
- **启动校验**: `ModelConfigLoader` 在应用就绪后检查 DB 中每种 App 类型是否有活跃配置,并对 DashScope 提供商比较 DB 与 yml 的 API Key 一致性 - **启动校验**: `ModelConfigLoader` 在应用就绪后检查 DB 中每种 App 类型是否有活跃配置,并对 DashScope 提供商比较 DB 与 yml 的 API Key 一致性

1
DEPLOY.md

@ -459,7 +459,6 @@ tail -f /opt/support-bot/logs/startup.out
| App 类型 | 用途 | 默认模型 | | App 类型 | 用途 | 默认模型 |
|----------|------|----------| |----------|------|----------|
| CHAT | 主对话 | qwen-turbo | | CHAT | 主对话 | qwen-turbo |
| PRODUCT_EXTRACT | 结构化数据提取 | qwen-turbo |
| EMBEDDING | 知识库向量化 | text-embedding-v2 | | EMBEDDING | 知识库向量化 | text-embedding-v2 |
| RAG_REWRITE | RAG 查询重写 | qwen-turbo | | RAG_REWRITE | RAG 查询重写 | qwen-turbo |

4
README.md

@ -164,8 +164,7 @@ src/main/java/com/wok/supportbot/
│ ├── MyLoggerAdvisor.java # 日志记录顾问 │ ├── MyLoggerAdvisor.java # 日志记录顾问
│ └── ReReadingAdvisor.java # 重读机制顾问 │ └── ReReadingAdvisor.java # 重读机制顾问
├── app/ # 核心应用服务 ├── app/ # 核心应用服务
│ ├── AssistantApp.java # 智能客服应用
│ └── ProductInfoApp.java # 商品信息提取应用
│ └── AssistantApp.java # 智能客服应用
├── chatmemory/ # 聊天记忆管理 ├── chatmemory/ # 聊天记忆管理
│ ├── DatabaseChatMemory.java # 数据库记忆存储 │ ├── DatabaseChatMemory.java # 数据库记忆存储
│ └── FileBasedChatMemory.java # 文件记忆存储 │ └── FileBasedChatMemory.java # 文件记忆存储
@ -193,7 +192,6 @@ src/main/java/com/wok/supportbot/
│ └── MyKeywordEnricher.java # AI 关键词提取 │ └── MyKeywordEnricher.java # AI 关键词提取
├── entity/ # 数据实体类 ├── entity/ # 数据实体类
│ ├── ChatMessage.java # 聊天消息实体 │ ├── ChatMessage.java # 聊天消息实体
│ ├── ProductInfo.java # 商品信息实体
│ ├── KnowledgeDocument.java # 知识文档实体 │ ├── KnowledgeDocument.java # 知识文档实体
│ ├── KnowledgeCategory.java # 知识分类实体 │ ├── KnowledgeCategory.java # 知识分类实体
│ ├── CategoryNode.java # 分类树节点 DTO │ ├── CategoryNode.java # 分类树节点 DTO

57
src/main/java/com/wok/supportbot/app/ProductInfoApp.java

@ -1,57 +0,0 @@
package com.wok.supportbot.app;
import com.wok.supportbot.advisor.MyLoggerAdvisor;
import com.wok.supportbot.config.ChatModelFactory;
import com.wok.supportbot.entity.ProductInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.stereotype.Component;
/**
* 电商商品信息抽取助手App - 支持多提供商动态切换
*/
@Component
@Slf4j
public class ProductInfoApp {
private final ChatModelFactory chatModelFactory;
private static final String SYSTEM_PROMPT = "你是一名电商商品信息抽取助手," +
"请从用户提供的商品网页内容中提取标题(title)、描述(description)、价格(price)、评分(rating)、评论数(reviewCount)、品牌(brand)、分类(category)等字段。" +
"请严格按照JSON格式返回,不要带任何解释和多余内容。";
public ProductInfoApp(ChatModelFactory chatModelFactory) {
this.chatModelFactory = chatModelFactory;
}
/**
* 商品信息结构化抽取
* @param rawContent 爬取的商品网页内容
* @return 结构化的商品信息对象
*/
public ProductInfo extractProductInfo(String rawContent) {
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository())
.build();
ChatClient chatClient = ChatClient.builder(chatModelFactory.getChatModel("PRODUCT_EXTRACT"))
.defaultSystem(SYSTEM_PROMPT)
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(chatMemory).build(),
new MyLoggerAdvisor()
)
.build();
ProductInfo productInfo = chatClient
.prompt()
.system(SYSTEM_PROMPT)
.user(rawContent)
.call()
.entity(ProductInfo.class);
log.info("Extracted product info: {}", productInfo);
return productInfo;
}
}

2
src/main/java/com/wok/supportbot/config/ChatModelFactory.java

@ -67,7 +67,7 @@ public class ChatModelFactory {
* 按应用类型获取活跃的 ChatModel * 按应用类型获取活跃的 ChatModel
* 如果该类型无活跃配置抛出异常不应在缺少配置时静默回退 * 如果该类型无活跃配置抛出异常不应在缺少配置时静默回退
* *
* @param appType 应用类型CHAT / PRODUCT_EXTRACT / EMBEDDING / RAG_REWRITE
* @param appType 应用类型CHAT / EMBEDDING / RAG_REWRITE
* @return ChatModel 实例 * @return ChatModel 实例
*/ */
public ChatModel getChatModel(String appType) { public ChatModel getChatModel(String appType) {

1
src/main/java/com/wok/supportbot/config/ModelConfigLoader.java

@ -28,7 +28,6 @@ public class ModelConfigLoader implements ApplicationListener<ApplicationReadyEv
log.info("========== AI 模型配置校验 =========="); log.info("========== AI 模型配置校验 ==========");
try { try {
checkAppTypeConfig("CHAT"); checkAppTypeConfig("CHAT");
checkAppTypeConfig("PRODUCT_EXTRACT");
checkAppTypeConfig("EMBEDDING"); checkAppTypeConfig("EMBEDDING");
checkAppTypeConfig("RAG_REWRITE"); checkAppTypeConfig("RAG_REWRITE");
log.info("========== AI 模型配置校验完成 =========="); log.info("========== AI 模型配置校验完成 ==========");

17
src/main/java/com/wok/supportbot/controller/AiController.java

@ -1,10 +1,7 @@
package com.wok.supportbot.controller; package com.wok.supportbot.controller;
import cn.hutool.json.JSONUtil;
import com.wok.supportbot.app.AssistantApp; import com.wok.supportbot.app.AssistantApp;
import com.wok.supportbot.app.ProductInfoApp;
import com.wok.supportbot.config.RoleAccessConfig; import com.wok.supportbot.config.RoleAccessConfig;
import com.wok.supportbot.entity.ProductInfo;
import com.wok.supportbot.service.ConversationService; import com.wok.supportbot.service.ConversationService;
import com.wok.supportbot.service.CustomerAccountService; import com.wok.supportbot.service.CustomerAccountService;
import com.wok.supportbot.service.CustomerAccountService.AccountScope; import com.wok.supportbot.service.CustomerAccountService.AccountScope;
@ -38,8 +35,6 @@ public class AiController {
@Resource @Resource
private AssistantApp assistantApp; private AssistantApp assistantApp;
@Resource @Resource
private ProductInfoApp productInfoApp;
@Resource
private CustomerServiceRoleService customerServiceRoleService; private CustomerServiceRoleService customerServiceRoleService;
@Resource @Resource
private CustomerAccountService customerAccountService; private CustomerAccountService customerAccountService;
@ -49,18 +44,6 @@ public class AiController {
private RoleAccessConfig roleAccessConfig; private RoleAccessConfig roleAccessConfig;
/**
* 同步调用 AI 提取结构化商品信息
*
* @param message
* @return
*/
@GetMapping("/product_info_app/chat/sync")
public String doChatWithProductInfoAppSync(String message) {
ProductInfo productInfo = productInfoApp.extractProductInfo(message);
return JSONUtil.toJsonStr(productInfo);
}
/** /**
* 同步调用 AI 智能客服应用 * 同步调用 AI 智能客服应用
* *

2
src/main/java/com/wok/supportbot/entity/AiModelConfig.java

@ -42,7 +42,7 @@ public class AiModelConfig implements Serializable {
private String name; private String name;
/** /**
* 应用类型CHAT / PRODUCT_EXTRACT / EMBEDDING / RAG_REWRITE
* 应用类型CHAT / EMBEDDING / RAG_REWRITE
*/ */
@TableField("app_type") @TableField("app_type")
private String appType; private String appType;

14
src/main/java/com/wok/supportbot/entity/ProductInfo.java

@ -1,14 +0,0 @@
package com.wok.supportbot.entity;
import lombok.Data;
@Data
public class ProductInfo {
private String title;
private String description;
private String price;
private String rating;
private Integer reviewCount;
private String brand;
private String category;
}

4
src/main/java/com/wok/supportbot/service/AiModelConfigService.java

@ -212,7 +212,7 @@ public class AiModelConfigService {
/** /**
* 激活指定配置 app_type 互斥 * 激活指定配置 app_type 互斥
* 校验Embedding 模型不能作为 CHAT / PRODUCT_EXTRACT / RAG_REWRITE 类型激活
* 校验Embedding 模型不能作为 CHAT / RAG_REWRITE 类型激活
* *
* @param id 配置ID * @param id 配置ID
*/ */
@ -292,7 +292,7 @@ public class AiModelConfigService {
/** /**
* 校验模型名称与应用类型的匹配关系 * 校验模型名称与应用类型的匹配关系
* Embedding 模型模型名含 "embedding"不能用于 CHAT / PRODUCT_EXTRACT / RAG_REWRITE 类型
* Embedding 模型模型名含 "embedding"不能用于 CHAT / RAG_REWRITE 类型
* 否则调用 /chat/completions 端点会返回 404 * 否则调用 /chat/completions 端点会返回 404
*/ */
private void validateModelTypeMatch(AiModelConfig config) { private void validateModelTypeMatch(AiModelConfig config) {

5
src/main/resources/static/components/ModelConfigManager.js

@ -10,7 +10,6 @@ import { toast, formatDate } from '../js/utils.js'
const APP_TYPE_OPTIONS = [ const APP_TYPE_OPTIONS = [
{ value: '', label: '全部类型' }, { value: '', label: '全部类型' },
{ value: 'CHAT', label: '智能客服对话' }, { value: 'CHAT', label: '智能客服对话' },
{ value: 'PRODUCT_EXTRACT', label: '商品信息抽取' },
{ value: 'EMBEDDING', label: '文本向量化' }, { value: 'EMBEDDING', label: '文本向量化' },
{ value: 'RAG_REWRITE', label: 'RAG查询重写' } { value: 'RAG_REWRITE', label: 'RAG查询重写' }
] ]
@ -542,12 +541,12 @@ export default {
// ==================== 工具函数 ==================== // ==================== 工具函数 ====================
function getAppTypeLabel(appType) { function getAppTypeLabel(appType) {
const map = { CHAT: '智能客服对话', PRODUCT_EXTRACT: '商品信息抽取', EMBEDDING: '文本向量化', RAG_REWRITE: 'RAG查询重写' }
const map = { CHAT: '智能客服对话', EMBEDDING: '文本向量化', RAG_REWRITE: 'RAG查询重写' }
return map[appType] || appType return map[appType] || appType
} }
function getAppTypeBadgeClass(appType) { function getAppTypeBadgeClass(appType) {
const map = { CHAT: 'badge-get', PRODUCT_EXTRACT: 'badge-post', EMBEDDING: '', RAG_REWRITE: '' }
const map = { CHAT: 'badge-get', EMBEDDING: '', RAG_REWRITE: '' }
return map[appType] || '' return map[appType] || ''
} }

32
src/test/java/com/wok/supportbot/SupportBotApplicationTests.java

@ -1,12 +1,9 @@
package com.wok.supportbot; package com.wok.supportbot;
import com.wok.supportbot.app.AssistantApp; import com.wok.supportbot.app.AssistantApp;
import com.wok.supportbot.app.ProductInfoApp;
import com.wok.supportbot.entity.ProductInfo;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import java.util.UUID; import java.util.UUID;
@ -16,9 +13,6 @@ class SupportBotApplicationTests {
@Resource @Resource
private AssistantApp assistantApp; private AssistantApp assistantApp;
@Autowired
private ProductInfoApp productInfoApp;
@Test @Test
void testChat() { void testChat() {
String chatId = UUID.randomUUID().toString(); String chatId = UUID.randomUUID().toString();
@ -38,32 +32,6 @@ class SupportBotApplicationTests {
Assertions.assertNotNull(answer); Assertions.assertNotNull(answer);
} }
@Test
public void testExtractProductInfo() {
// 模拟爬取的网页内容建议写简洁但包含关键信息
String rawContent = "这是商品标题:智能手表Pro 2025," +
"描述:这款智能手表支持心率监测和GPS," +
"价格:299美元,评分:4.7星,评论数:1567,品牌:TechBrand,分类:电子产品。";
// 调用方法
ProductInfo productInfo = productInfoApp.extractProductInfo(rawContent);
// 断言结果不为空
Assertions.assertNotNull(productInfo);
// 断言关键字段合理你也可以根据实际字段调整
Assertions.assertNotNull(productInfo.getTitle());
Assertions.assertTrue(productInfo.getTitle().contains("智能手表"));
Assertions.assertNotNull(productInfo.getPrice());
Assertions.assertTrue(productInfo.getPrice().contains("299"));
Assertions.assertNotNull(productInfo.getBrand());
Assertions.assertEquals("TechBrand", productInfo.getBrand());
// 你可以打印结果方便调试
System.out.println("提取的商品信息: " + productInfo);
}
@Test @Test
void doChatWithRag() { void doChatWithRag() {
String chatId = "1069b88d-eb85-47ac-bd2e-c393d118a5aa"; String chatId = "1069b88d-eb85-47ac-bd2e-c393d118a5aa";

Loading…
Cancel
Save