chat(
- @MemoryId String userId,
- @V("userInput") String userInput,
- @V("sSystemPrompt") String sSystemPrompt
- );
- /**
- * 动态表结构:自然语言解释SQL执行结果
- * 入参:用户问题、执行的SQL、表结构、JSON格式结果
- */
- @SystemMessage("""
- 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景:
- 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
- 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
- 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”
- 3.1. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 1000000,错误写法是 1,000,000,即使数字很大,也请保持连续的数字串,不要打断。
- 3.2 所有日期请转换为 YYYY-MM-DD 格式(例如:2026-03-15),严禁包含时间部分(如小时、分钟、秒)(例如:2026-03-15 00:00:00),也不要包含时区信息。”
- 3.3. 金额,单价,数量 严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 2400056,错误写法是 2,400,056 即使数字很大,也请保持连续的数字串,不要打断。
- 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
- 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
- """)
- @UserMessage("""
- 【业务场景表结构信息】
- 表结构详情:{{tableStruct}}
- 【查询相关信息】
- 用户原始查询:{{userInput}}
- 执行的MySQL SQL:{{sql}}
- SQL执行结果(JSON格式):{{result}}
- 请根据上述信息+通用规则,对查询结果做业务解释:
- """)
- String explainSqlResult(@MemoryId String userId,
- @V("userInput") String userInput,
- @V("sql") String sql,
- @V("tableStruct") String tableStruct,
- @V("result") String result);
-
- /**
- * 动态表结构:自然语言解释SQL执行结果
- * 入参:用户问题、执行的SQL、表结构、JSON格式结果
- */
- @SystemMessage("""
- 你是专业的业务数据分析师,请分析以下查询结果:
- 【用户问题】
- {{userInput}}
- 【数据字段说明】
- {{sMilvusFiledDescription}}
- 【查询结果数据(JSON格式)】
- {{result}}
- 【分析要求】
- 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
- 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
- 3. 输出格式:
- 3.1. 如果用户要求"表格形式展示",先输出简短文字说明,然后输出Markdown格式的表格
- 3.2. 如果用户未要求表格,仅返回解释内容,不要列出ID,无多余标题、换行、符号
- 3.3. 结果为空时直接返回"未查询到相关数据"
- 3.4. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")
- 3.5. 所有日期请转换为 YYYY-MM-DD 格式,严禁包含时间部分
- 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
- 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
- """)
- @UserMessage("""
- 【用户查询】
- {{userInput}}
- 【字段说明】
- {{sMilvusFiledDescription}}
- 【查询结果】
- 用户原始查询:{{userInput}}
- 执行查询向量库后结果(JSON格式):{{result}}
- 请根据上述信息+通用规则,对查询结果做业务解释:
- """)
- String explainMilvusResult(@MemoryId String userId,
- @V("userInput") String userInput,
- @V("sMilvusFiledDescription") String sMilvusFiledDescription,
- @V("result") String result);
-
- @SystemMessage("""
- 你是一个智能查询路由专家。请根据【用户需求】,只返回 true 或 false。
- 【最高优先级规则 - 必须首先判断】
- 如果用户需求包含以下任一关键词,**直接返回 false**,不再进行其他判断:
- - 明细、列表、清单
- - ...明细、...列表、...清单
- 重要:只要出现以上关键词,说明用户需要的是明细数据查询,而非统计分析。
- 【统计类关键词 - 仅在满足最高优先级规则后才判断】
- 只有当用户需求不包含上述明细类关键词时,才检查是否包含以下关键词:
- 统计、求和、汇总、排名、TopN、平均、数量、总额、最高、最低、占比、分组
- - 如果包含,返回 true
- - 否则返回 false
- 【判断示例】
- - \"查询中科精工集团的彩盒类产品的报价单明细\" → false(包含\"明细\")
- - \"查询客户张三信息\" → false(无统计关键词,无明细关键词)
- - \"销售额排名前10的产品\" → true(包含\"排名\",且无明细关键词)
- - \"查看销售订单明细\" → false(包含\"明细\")
- """)
- @UserMessage("""
- 【用户需求】
- {{userInput}}
- """)
-// @SystemMessage("""
-// 你是一个智能查询路由专家,请根据【用户需求】,基于**查询效率最优**原则,自动判断使用关系型数据库(MySQL)还是向量库(Milvus),只返回 true 或 false。
-// 满足如下规则中任意一条则返回true 否则返回false:
-// - 查询涉及**排名、TOP N、求和、计数、平均值、最大值、总额、最小值、最高、最低、占比**
-// - 查询涉及**分组统计(GROUP BY)、排序(ORDER BY)、分页(LIMIT)**
-// """)
-// @UserMessage("""
-// 【用户需求】
-// {{userInput}}
-// """)
- Boolean routeQuery(@MemoryId String userId, @V("userInput") String userInput);
-
- /**
- * 生成 Milvus 过滤条件(适配 Milvus v2.3.9)
- */
- @SystemMessage("""
- MILVUS 查询条件生成规则:
-
- 【最高优先级 - 输出格式铁律】
- ⚠️ 你的【全部输出】必须是且仅是一个合法的 JSON 对象
- ⚠️ 禁止输出任何解释、说明、思考过程
- ⚠️ 禁止输出任何中文文字
- ⚠️ 只能输出以下 JSON 格式,不能有其他任何内容
-
- 【输出 JSON 结构】
- {
- "sMethodName": true/false, // 必选,判断用户意图是否匹配当前方法
- "vectorField": "向量字段名", // 可选,需要语义匹配时返回
- "vectorValue": "向量化文本", // 可选,用于向量检索的文本
- "filterExpression": "标量过滤表达式" // 可选,有标量条件时返回
- }
-
- 【方法匹配规则 - 重要】
- sMethodName 的取值逻辑:
- - 如果用户输入的意图与 {{sMethodName}} 相关,返回 true
- - 否则返回 false
-
- 判断标准:用户是否在询问或操作与 {{sMethodName}} 相关的业务数据
- - 包括但不限于:查询、搜索、推荐、找、查看、明细、列表、详情等
- - 只要用户想获取或操作这类数据,就应该返回 true
-
- 示例:
- - 方法名称:查询报价单
- - "报价单明细" → true(想查看报价单数据)
- - "报价单列表" → true(想查看报价单数据)
- - "报价单" → true(想查看报价单数据)
- - "查一下报价单" → true(想查询报价单)
- - "推荐相似报价单" → true(想推荐报价单)
- - "你好" → false(与报价单无关)
- - "查询客户信息" → false(与报价单无关)
-
- 【重要:理解你的数据结构】
-
- 你有两种类型的字段:
-
- 1. 标量字段(用于精确过滤):
- - {{sMilvusFiled}} 中的字段
- - 根据提供的字段说明使用
-
- 2. 向量字段(用于语义搜索):
- - 字段名:使用 {{sMilvusFiledXl}} 中提供的向量字段名
- - 存储格式:管道符分隔的键值对 "字段名:值|字段名:值|..."
- - 包含的业务数据:{{sMilvusFiledDescriptionXl}}
- - 示例:"sCustomerName:上海小羚羊|sProductName:2028宣传海报|dProductQty:1000|sType:报价丢单原因"
-
- 【向量搜索规则】
-
- 使用向量搜索的条件:用户明确表达语义匹配意图
- - 关键词:找相似、推荐、匹配、类似、相关、类似的、相似的、推荐一下
- - 使用向量搜索时:
- 1. 从用户问题中提取关键业务实体
- 2. 格式化为:"字段名:值|字段名:值"(管道符分隔)
- 3. 示例:
- - 用户:"找丢单原因类似价格太高的记录" → "sReason:价格太高|sType:报价丢单原因"
- - 用户:"推荐和上海小羚羊类似的客户" → "sCustomerName:上海小羚羊"
- - 用户:"找类似2028宣传海报的产品" → "sProductName:2028宣传海报"
-
- 【标量过滤规则 - 极其重要】
-
- 可用标量字段(根据 {{sMilvusFiled}} 动态提供):
- - 使用提供的字段名
- - 【字符串字段】:统一使用 like 操作符(前缀匹配)
- - 【数字字段】:使用 >、<、>=、<=、== 操作符
- - 【时间字段】:使用时间戳范围
-
- 【操作符使用规则】
-
- 1. 字符串字段(统一使用 like):
- - 语法:字段名 like '关键词%'
- - 说明:只支持前缀匹配,不支持 '%keyword%' 或 '%keyword'
- - 示例:
- - 用户:"上海小羚羊" → sCustomerName like '上海小羚羊%'
- - 用户:"客户名称包含上海" → sCustomerName like '上海%'
- - 用户:"姓张的销售人员" → sSalesManName like '张%'
- - 用户:"产品名称以2028开头" → sProductName like '2028%'
- - 用户:"张三" → sSalesManName like '张三%'
-
- 2. 数字字段:
- - 精确匹配:字段名 == 值
- - 范围匹配:字段名 >= 值 && 字段名 <= 值
- - 大于:字段名 > 值
- - 小于:字段名 < 值
- - 示例:
- - 用户:"数量1000" → dProductQty == 1000
- - 用户:"数量大于1000" → dProductQty > 1000
- - 用户:"数量在500到1000之间" → dProductQty >= 500 && dProductQty <= 1000
-
- 3. 时间字段:
- - 使用时间戳范围:tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳
- - 时间戳为 Unix 秒
-
- 4. 组合条件:
- - 使用 && (AND) 连接多个条件
- - 使用 || (OR) 连接或条件
- - 复杂条件用括号分组
-
- 【核心约束 - 必须遵守】
- 1. 只有当用户【明确指定】具体条件时,才生成 filterExpression
- 2. 禁止为模糊查询添加默认过滤条件(如时间范围)
- 3. 禁止自动添加任何默认条件
- 4. 当前时间戳:{{sDataNow}}(Unix秒)
- 5. 字符串字段禁止使用 ==,必须使用 like
- 6. like 只支持前缀匹配,不支持 '%keyword%' 或 '%keyword' 格式
-
- 【查询意图判断】
-
- 类型A - 纯标量查询(只返回 filterExpression):
- - 用户明确指定了标量字段的具体条件
- - 不包含语义匹配词
- - 示例1:"上海小羚羊" → {"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%'"}
- - 示例2:"客户名称包含上海" → {"sMethodName": true, "filterExpression": "sCustomerName like '上海%'"}
- - 示例3:"张三" → {"sMethodName": true, "filterExpression": "sSalesManName like '张三%'"}
- - 示例4:"数量大于1000" → {"sMethodName": true, "filterExpression": "dProductQty > 1000"}
- - 示例5:"今天创建的报价单" → {"sMethodName": true, "filterExpression": "tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳"}
-
- 类型B - 纯向量搜索(只返回 vectorField 和 vectorValue):
- - 用户只有语义意图,无具体标量条件
- - 示例:"找一些类似的报价单" → {"sMethodName": true, "vectorField": "向量字段名", "vectorValue": "sType:报价丢单原因"}
-
- 类型C - 混合查询(同时返回 vectorField、vectorValue 和 filterExpression):
- - 用户既有具体条件,又需要语义匹配
- - 示例:"找丢单原因类似价格太高的上海小羚羊报价单" →
- {"sMethodName": true, "vectorField": "向量字段名", "vectorValue": "sReason:价格太高|sType:报价丢单原因", "filterExpression": "sCustomerName like '上海小羚羊%'"}
-
- 类型D - 意图不匹配(只返回 sMethodName: false):
- - 用户输入与 {{sMethodName}} 完全无关
- - 示例:"你好" → {"sMethodName": false}
- - 示例:"查询客户信息"(如果方法名是"查询报价单") → {"sMethodName": false}
-
- 类型E - 意图匹配但无具体条件(只返回 sMethodName: true):
- - 用户想查询该类数据,但没有指定任何条件
- - 示例:"报价单明细" → {"sMethodName": true}
- - 示例:"报价单列表" → {"sMethodName": true}
- - 示例:"报价单" → {"sMethodName": true}
-
- 【时间处理规则】
-
- 当用户明确提到时间时,基于当前时间 {{sDataNow}} 计算:
- - "今天":当天 00:00:00 到 23:59:59
- - "昨天":前一天 00:00:00 到 23:59:59
- - "本周":本周一 00:00:00 到本周日 23:59:59
- - "本月":本月1日 00:00:00 到本月最后一天 23:59:59
- - "上个月":上个月1日 00:00:00 到上个月最后一天 23:59:59
- - "近X天":X天前 00:00:00 到今天 23:59:59
-
- 时间范围表达式格式:tCreateDate >= 开始时间戳 && tCreateDate <= 结束时间戳
-
- 【重要约束汇总】
- 1. 只输出 JSON,不要有任何其他内容
- 2. 只使用提供的标量字段
- 3. 【关键】字符串字段必须使用 like,禁止使用 ==
- 4. 时间范围必须使用 Unix 时间戳(秒)
- 5. 向量字段名使用 {{sMilvusFiledXl}} 中提供的字段名
- 6. vectorValue 必须保持管道符分隔格式:"字段名:值|字段名:值"
- 7. 禁止为模糊查询添加默认条件
- 8. sMethodName 只要用户意图与当前方法相关就返回 true
- 9. like 只支持前缀匹配,格式:字段名 like '值%'
-
- 【输出示例】
- 方法名称:查询报价单
-
- 示例1:模糊查询 - "报价单明细"
- 输出:{"sMethodName": true}
-
- 示例2:字符串模糊匹配 - "上海小羚羊"
- 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%'"}
-
- 示例3:字符串模糊匹配 - "客户名称包含上海"
- 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海%'"}
-
- 示例4:字符串模糊匹配 - "张三"
- 输出:{"sMethodName": true, "filterExpression": "sSalesManName like '张三%'"}
-
- 示例5:数字范围 - "数量大于1000"
- 输出:{"sMethodName": true, "filterExpression": "dProductQty > 1000"}
-
- 示例6:时间范围 - "今天创建的报价单"
- 输出:{"sMethodName": true, "filterExpression": "tCreateDate >= 1774754400 && tCreateDate <= 1774840799"}
-
- 示例7:组合条件 - "上海小羚羊且数量大于1000"
- 输出:{"sMethodName": true, "filterExpression": "sCustomerName like '上海小羚羊%' && dProductQty > 1000"}
-
- 示例8:纯向量 - "找一些类似的报价单"
- 输出:{"sMethodName": true, "vectorField": "content_embedding", "vectorValue": "sType:报价丢单原因"}
-
- 示例9:混合查询 - "找丢单原因类似价格太高的上海小羚羊报价单"
- 输出:{"sMethodName": true, "vectorField": "content_embedding", "vectorValue": "sReason:价格太高|sType:报价丢单原因", "filterExpression": "sCustomerName like '上海小羚羊%'"}
-
- 示例10:意图不匹配 - "你好"
- 输出:{"sMethodName": false}
-
- 【标量字段列表】
- {{sMilvusFiled}}
-
- 【标量字段说明】
- {{sMilvusFiledDescription}}
-
- 【向量字段列表】
- {{sMilvusFiledXl}}
-
- 【向量字段说明】
- {{sMilvusFiledDescriptionXl}}
-
- 【方法名称】
- {{sMethodName}}
-
- """)
- @UserMessage("""
- 【用户输入】
- {{userInput}}
-
- 【当前时间】
- {{sDataNow}}
-
- 【标量字段】
- {{sMilvusFiled}}
-
- 【标量字段说明】
- {{sMilvusFiledDescription}}
-
- 【向量字段】
- {{sMilvusFiledXl}}
-
- 【向量字段说明】
- {{sMilvusFiledDescriptionXl}}
-
- 【方法名称】
- {{sMethodName}}
-
- 请根据以上规则,输出 JSON 格式结果。
-""")
- String getMilvusFilter(@MemoryId String userId,
- @V("userInput") String userInput,
- @V("sMilvusFiled") String sMilvusFiled,
- @V("sMilvusFiledDescription") String sMilvusFiledDescription,
- @V("sMilvusFiledXl") String sMilvusFiledXl,
- @V("sMilvusFiledDescriptionXl") String sMilvusFiledDescriptionXl,
- @V("sDataNow") long sDataNow,
- @V("sMethodName") String sMethodName
- );
-}
diff --git a/src/main/java/com/xly/agent/Intent.java b/src/main/java/com/xly/agent/Intent.java
index 642ace2..b233dc0 100644
--- a/src/main/java/com/xly/agent/Intent.java
+++ b/src/main/java/com/xly/agent/Intent.java
@@ -8,7 +8,7 @@ import java.util.List;
*
* 由 {@code IntentService} 用受约束 JSON 一次得出:把一句自然语言压成
* {意图, 单据/实体类型, 带业务角色的实体清单, 还缺什么}。下游据此做**确定性路由**,
- * 而不是让弱模型在 12 个工具里盲选——这是治理「理解错/选错工具」的核心。
+ * 而不是让弱模型在全部 9 个工具里盲选——这是治理「理解错/选错工具」的核心。
*/
public class Intent {
diff --git a/src/main/java/com/xly/agent/ReActAgent.java b/src/main/java/com/xly/agent/ReActAgent.java
index 0918971..8976164 100644
--- a/src/main/java/com/xly/agent/ReActAgent.java
+++ b/src/main/java/com/xly/agent/ReActAgent.java
@@ -8,7 +8,11 @@ import dev.langchain4j.service.UserMessage;
* 单一 ReAct 智能体。
*
*
LangChain4j 的 {@code AiServices} 原生工具调用循环即 ReAct:模型自行决定是否调用工具、
- * 调用哪个工具、以及何时给出最终答复。不再有旧的 8 场景 SceneSelector 路由——由 agent 自路由。
+ * 调用哪个工具、以及何时给出最终答复。不再有旧的 8 场景 {@code SceneSelector} 路由。
+ *
+ *
但 agent 也**不是完全自路由**:{@code AgentChatController} 先用意图门({@code IntentService})
+ * 判类,再由 {@link com.xly.config.AgentFactory} 按 {@link ToolScope} 只把该意图相关的工具装进本实例
+ * (新增意图更是完全绕过 ReAct,走确定性 collectForm 路径)。本接口只负责「给定这批工具,自己决定怎么用」。
*
*
返回 {@link TokenStream} 以支持流式输出;{@link MemoryId} 绑定每个会话独立的对话记忆。
*/
diff --git a/src/main/java/com/xly/agent/SceneSelectorAiAgent.java b/src/main/java/com/xly/agent/SceneSelectorAiAgent.java
deleted file mode 100644
index 39f79eb..0000000
--- a/src/main/java/com/xly/agent/SceneSelectorAiAgent.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.xly.agent;
-
-import com.xly.entity.SceneIntentParseResp;
-import dev.langchain4j.service.SystemMessage;
-import dev.langchain4j.service.UserMessage;
-import dev.langchain4j.service.V;
-import org.springframework.stereotype.Component;
-
-/**
- * 场景意图解析AI服务:专门让大模型解析用户输入的意图,匹配对应的业务场景
- * 基于LangChain4j AiServices构建,由大模型返回标准化的场景编码
- */
-public interface SceneSelectorAiAgent {
-
- /**
- * 核心方法:解析用户意图,匹配业务场景
- * @param userInput 用户输入
- * @param authScenesDesc 可访问场景描述
- * @return 标准化的意图解析响应(仅返回sceneCode)
- */
- @SystemMessage("""
- 你是专业的ERP系统**意图解析助理**,你的唯一职责是根据用户输入,匹配其意图对应的业务场景,严格遵循以下规则:
- 1. 仅从用户提供的「可访问场景列表」中选择匹配的场景,绝对不允许虚构场景;
- 2. 匹配规则:用户输入的意图与场景的「支持功能」高度相关,即匹配该场景;
- 3. 输出格式:必须严格返回JSON格式,仅包含sceneCode字段,无任何多余文字、解释、换行;
- - 匹配到一个场景:sceneCode为场景码(如salemange/purchasemange/ProductionR&Dmanagement),scene为场景名称(如销售管理/采购管理/生产管理);
- - 匹配到多个场景,请列出并让客户选择场景;
- - 无匹配场景/用户仅问候:sceneCode为NO_MATCH;
- 4. 不允许添加任何额外字段,不允许返回JSON以外的内容,确保后端能直接解析;
- 5. 忽略用户输入中的无关语气词(如“你好”“帮我”“麻烦”),提取核心业务意图。
- """)
- @UserMessage("""
- 用户输入:{{userInput}}
- 可访问场景列表:{{authScenesDesc}}
- 请严格按指定格式返回匹配的场景编码!
- """)
- SceneIntentParseResp parseSceneIntent(@V("userInput") String userInput,
- @V("authScenesDesc") String authScenesDesc);
-}
\ No newline at end of file
diff --git a/src/main/java/com/xly/agent/SystemPromptGenerator.java b/src/main/java/com/xly/agent/SystemPromptGenerator.java
deleted file mode 100644
index d18d658..0000000
--- a/src/main/java/com/xly/agent/SystemPromptGenerator.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.xly.agent;
-
-import cn.hutool.core.util.ObjectUtil;
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONObject;
-import com.alibaba.fastjson2.JSONWriter;
-import com.xly.entity.ParamRule;
-import com.xly.entity.UserSceneSession;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-public class SystemPromptGenerator {
-
- public static String generate(String toolName, UserSceneSession session) {
- StringBuilder sb = new StringBuilder();
-
- sb.append("你是一个无情的参数提取器,不是聊天助手。\n\n");
- sb.append("【核心任务】\n");
- sb.append("从用户输入中提取参数,并立即调用工具 ").append(toolName).append("。\n\n");
- sb.append("【绝对指令】\n");
- sb.append("1. 严禁与用户对话,严禁输出任何纯文本回复。\n");
- sb.append("2. 无论参数缺失多么严重,都必须直接调用工具,缺失字段填空字符串或0。\n");
- sb.append("3. 不要引导用户,后端程序会处理引导逻辑。\n\n");
- sb.append("【参数提取规则】\n");
- sb.append("用户输入格式为 \"参数名: 值\" 或 \"参数名 值\",你必须识别参数名并提取对应的值。\n\n");
- sb.append("【可提取的参数列表】\n");
- List dynamicParamRuleAll = session.getCurrentTool().getParamRuleListAll();
- // 从参数定义中提取
- for (ParamRule paramRule : dynamicParamRuleAll) {
- sb.append("- ").append(paramRule.getSParam()).append("\n");
- }
- sb.append("\n【提取示例】\n");
- // 动态生成示例(取前3个参数作为示例)
- int count = 0;
- for (ParamRule paramRule : dynamicParamRuleAll) {
- if (count++ >= 3) break;
- sb.append("- 用户输入 \"").append(paramRule.getSParam()).append(": {值}\" → 提取 ").append(paramRule.getSParam()).append("\n");
- }
-
- if(ObjectUtil.isNotEmpty(session.getArgs())){
- sb.append("【已提取参数】\n");
- sb.append(JSON.toJSONString(session.getArgs(), JSONWriter.Feature.PrettyFormat));
- }
- sb.append("\n【工具调用规则】\n");
- sb.append("1. 每次只调用一次工具\n");
- sb.append("2. 调用后立即停止,不要重复调用\n");
- sb.append("3. 不要总结,不要回复用户,只调用工具");
-
- return sb.toString();
- }
-
- /**
- * 缺失参数统一提示模板:强制AI调用自定义方法,一次性回填所有参数
- */
- public static String buildMissParamPrompt(UserSceneSession session, List paramRuleDataMiss) {
- String methodNo = session.getCurrentTool().getSMethodNo();
- // 1. 拼接缺失参数的描述(增加“参数名”和“英文名”的对应,方便模型映射)
- String paramDesc = paramRuleDataMiss.stream()
- .map(p -> String.format("- 参数名:%s (对应字段: %s),示例值:%s",
- p.getSParam(), p.getSParamValue(), p.getSExampleValue()))
- .collect(Collectors.joining("\n"));
- // 但根据你的需求,其实 prompt 里并不需要强行插入一个空的 JSON 片段,
- // 直接告诉模型去“合并”即可。
- return String.format("""
- 请你根据用户最新的输入,补充缺失的参数,并带着【所有完整参数】立即发起工具调用。
-
- 【核心执行规则】
- 1. **参数合并(至关重要)**:你必须保留上一轮对话中已经获取的参数(如 sSlaveId, operateType 等),并将用户本次提供的新参数与旧参数合并。
- 2. **必须调用工具**:必须调用工具 `%s`。绝对禁止直接输出 JSON 文本,也绝对禁止向用户反问或索要信息。
- 3. **完整调用**:请直接生成包含所有参数的 tool_calls 请求。
- 【已获取数据】
- %s
- 【当前缺失的参数说明】
- %s
- """, methodNo,JSONObject.toJSONString(session.getArgs()), paramDesc);
- }
-
- public static String buildDynamicSystemPrompt(UserSceneSession session) {
- // 获取当前工具编号(用于 JSON 的 name 字段)
- String methodNo = session.getCurrentTool().getSMethodNo();
- return """
- 【极强约束·必须执行】
- 1. 禁止说话!禁止解释!
- 2. 必须调用工具!
- 3. 只输出标准工具调用JSON!
-
- ### 任务背景 ###
- 用户会输入自然语言指令(如“第1行确认”、“全部合并”等)。
- 你的任务是:解析出【操作类型】和【目标行号】。
-
- ### 解析规则(必须严格遵守) ###
-
- 1. **操作类型 (operateType)**:
- - 如果用户说“全部确认”、“生成多个单据”,识别为:"全部确认"
- - 如果用户说“合并确认”、“生成一个单据”,识别为:"合并确认"
- - 其他情况,默认为:"单行确认"
-
- 2. **行号提取 (rowNumbers)**:
- - 单选:如“第5行”,提取为 [5]
- - 多选:如“第1、3、5行”,提取为 [1, 3, 5]
- - 范围:如“第2到4行”,提取为 [2, 3, 4]
- - 全选:如“全部”、“所有”,提取为 ["ALL"]
-
- ### 输出格式规范 ###
- 请直接输出以下 JSON(不要任何 Markdown 格式,不要 ```json):
- {"name": "%s", "parameters": {"operateType": "解析出的类型", "rowNumbers": [行号数组或"ALL"]}}
-
- """.formatted(methodNo);
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/com/xly/agent/ToolScope.java b/src/main/java/com/xly/agent/ToolScope.java
index 492eaf0..8c041ee 100644
--- a/src/main/java/com/xly/agent/ToolScope.java
+++ b/src/main/java/com/xly/agent/ToolScope.java
@@ -4,14 +4,14 @@ package com.xly.agent;
* 按意图收窄「本轮可见的工具集」(架构 §5 的 tool scoping)。
*
* 依据:单次 ReAct 的选工具准确率随工具数量上升而骤降(业界经验:50 个工具 ~94% → 200 个 ~64%)。
- * 弱模型(qwen3:14b Q4)一次性面对 12 个语义重叠的工具时,频繁选错族(把「查询」当「新增」)。
- * 于是先用意图门判类,再**只暴露该意图相关的 3-5 个工具**给 ReAct 执行——大幅降低选错概率。
+ * 弱模型(qwen3:14b Q4)一次性面对全部 9 个语义重叠的工具时,频繁选错族(把「查询」当「新增」)。
+ * 于是先用意图门判类,再**只暴露该意图相关的 5 个工具**给 ReAct 执行——大幅降低选错概率。
*/
public enum ToolScope {
/** 读:findForms/kgSearch/readFormData/lookupRecord/queryData(+askUser/loadSkill)。 */
READ,
- /** 写:collectForm/proposeCreate/Update/Delete/Examine(+lookupRecord 定位、askUser/loadSkill)。 */
+ /** 写:collectForm/proposeWrite(action=…)(+readFormData·lookupRecord 定位、askUser/loadSkill)。 */
WRITE,
- /** 兜底:全部工具(意图门失败时用)。 */
+ /** 兜底:全部 9 个工具(意图门失败时用)。 */
FULL
}
diff --git a/src/main/java/com/xly/config/BizExecuteUtil.java b/src/main/java/com/xly/config/BizExecuteUtil.java
deleted file mode 100644
index 1e9b376..0000000
--- a/src/main/java/com/xly/config/BizExecuteUtil.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.xly.config;
-
-import groovy.lang.GroovyClassLoader;
-import groovy.lang.GroovyObject;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.expression.Expression;
-import org.springframework.expression.ExpressionParser;
-import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
-import org.springframework.stereotype.Component;
-
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * 业务逻辑执行工具:EL表达式 + Groovy脚本
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class BizExecuteUtil {
- /**
- * EL表达式解析器
- */
- private final ExpressionParser elParser = new SpelExpressionParser();
-
- /**
- * Groovy类加载器
- */
- private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader();
-
- /**
- * 执行业务逻辑
- * @param bizType 1=EL,2=Groovy
- * @param bizContent 逻辑内容
- * @param params JSON参数
- * @return 执行结果
- */
- public String execute(Integer bizType, String bizContent, Map params) {
- if (Objects.isNull(bizType) || Objects.isNull(bizContent) || bizContent.isBlank()) {
- throw new IllegalArgumentException("业务逻辑配置异常");
- }
- // EL表达式执行
- if (1 == bizType) {
- return executeEL(bizContent, params);
- }
- // Groovy脚本执行
- else if (2 == bizType) {
- return executeGroovy(bizContent, params);
- }
- else {
- throw new IllegalArgumentException("不支持的业务逻辑类型:" + bizType);
- }
- }
-
- /**
- * 执行EL表达式
- */
- private String executeEL(String elContent, Map params) {
- try {
- StandardEvaluationContext context = new StandardEvaluationContext();
- context.setVariable("params", params);
- Expression expression = elParser.parseExpression(elContent);
- return expression.getValue(context, String.class);
- } catch (Exception e) {
- log.error("EL表达式执行失败:{}", elContent, e);
- throw new IllegalArgumentException("EL表达式执行失败:" + e.getMessage());
- }
- }
-
- /**
- * 执行Groovy脚本
- */
- private String executeGroovy(String groovyContent, Map params) {
- try {
- // 构建Groovy脚本类
- String scriptCode = "class DynamicScript { def execute(Map params) { " + groovyContent + " } }";
- Class> groovyClass = groovyClassLoader.parseClass(scriptCode);
- GroovyObject groovyObject = (GroovyObject) groovyClass.getDeclaredConstructor().newInstance();
- // 执行脚本并返回结果
- Object result = groovyObject.invokeMethod("execute", new Object[]{params});
- return Objects.isNull(result) ? "执行成功" : result.toString();
- } catch (Exception e) {
- log.error("Groovy脚本执行失败:{}", groovyContent, e);
- throw new IllegalArgumentException("Groovy脚本执行失败:" + e.getMessage());
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/xly/config/JacksonConfig.java b/src/main/java/com/xly/config/JacksonConfig.java
deleted file mode 100644
index 0b4e512..0000000
--- a/src/main/java/com/xly/config/JacksonConfig.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.xly.config;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.SerializationFeature;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Primary;
-
-/**
- * 全局 ObjectMapper 配置:支持 LocalDate 序列化/反序列化
- */
-@Configuration
-public class JacksonConfig {
-
- /**
- * 配置好的 ObjectMapper:注册 JavaTimeModule,支持 Java 8 时间类型
- */
- @Bean
- @Primary // 标记为默认实例,避免多 ObjectMapper 冲突
- public ObjectMapper objectMapper() {
- ObjectMapper mapper = new ObjectMapper();
- // 1. 核心:注册 JSR310 模块,处理 LocalDate/LocalDateTime
- mapper.registerModule(new JavaTimeModule());
- // 2. 关闭时间戳序列化,LocalDate 以 "yyyy-MM-dd" 字符串形式存储
- mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
- // 3. 忽略未知字段(AI 返回多余字段时不报错)
- mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
- return mapper;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/xly/config/ModelConfig.java b/src/main/java/com/xly/config/ModelConfig.java
index a541497..fd994ff 100644
--- a/src/main/java/com/xly/config/ModelConfig.java
+++ b/src/main/java/com/xly/config/ModelConfig.java
@@ -3,13 +3,7 @@ package com.xly.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.xly.agent.DynamicTableNl2SqlAiAgent;
-import com.xly.agent.SceneSelectorAiAgent;
import dev.langchain4j.model.ollama.OllamaChatModel;
-import dev.langchain4j.model.ollama.OllamaStreamingChatModel;
-import dev.langchain4j.service.AiServices;
-import dev.langchain4j.memory.chat.MessageWindowChatMemory;
-import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -23,42 +17,10 @@ public class ModelConfig {
@Value("${langchain4j.ollama.base-url}")
private String chatModelUrl;
- @Value("${langchain4j.ollama.chat-model-name}")
- private String chatModelName;
-
@Value("${langchain4j.ollama.sql-model-name}")
private String sqlModelName;
- // ====================== 主对话模型 ======================
- @Bean
- @Primary
- public OllamaChatModel chatLanguageModel() {
- return OllamaChatModel.builder()
- .baseUrl(chatModelUrl)
- .modelName(chatModelName)
- .temperature(0.1)
- .topP(0.95)
- .timeout(Duration.ofSeconds(120))
- .maxRetries(2)
- .logRequests(true)
- .logResponses(true)
- .build();
- }
-
- // ====================== 自由闲聊模型 ======================
- @Bean("chatiModel")
- public OllamaChatModel chatiModel() {
- return OllamaChatModel.builder()
- .baseUrl(chatModelUrl)
- .modelName(chatModelName)
- .temperature(0.7)
- .topP(0.9)
- .timeout(Duration.ofSeconds(60))
- .maxRetries(2)
- .build();
- }
-
- // ====================== SQL 专用模型 ======================
+ /** NL2SQL 专用模型(QueryTool 经 AgentFactory 注入)。 */
@Bean("sqlChatModel")
public OllamaChatModel sqlChatModel() {
return OllamaChatModel.builder()
@@ -72,34 +34,6 @@ public class ModelConfig {
.build();
}
- // ====================== 流式对话模型 ======================
- @Bean("streamingChatModel")
- @Primary
- public OllamaStreamingChatModel streamingChatModel() {
- return OllamaStreamingChatModel.builder()
- .baseUrl(chatModelUrl)
- .modelName(chatModelName)
- .temperature(0.3)
- .topP(0.9)
- .numPredict(1024)
- .timeout(Duration.ofSeconds(60))
- .build();
- }
-
- // ====================== 流式 SQL 模型 ======================
- @Bean("streamingSqlModel")
- public OllamaStreamingChatModel streamingSqlModel() {
- return OllamaStreamingChatModel.builder()
- .baseUrl(chatModelUrl)
- .modelName(sqlModelName)
- .temperature(0.2)
- .topP(0.95)
- .numPredict(2048)
- .timeout(Duration.ofSeconds(120))
- .build();
- }
-
- // ====================== JSON ======================
@Bean
@Primary
public ObjectMapper objectMapper() {
@@ -109,24 +43,4 @@ public class ModelConfig {
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}
-
- // ====================== 动态 SQL Agent ======================
- @Bean
- public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent(
- @Qualifier("sqlChatModel") OllamaChatModel sqlModel) {
- return AiServices.builder(DynamicTableNl2SqlAiAgent.class)
- .chatModel(sqlModel)
- .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
- .build();
- }
-
- // ====================== 场景选择 Agent ======================
- @Bean
- public SceneSelectorAiAgent sceneSelectorAiAgent(
- @Qualifier("chatLanguageModel") OllamaChatModel chatLanguageModel) {
- return AiServices.builder(SceneSelectorAiAgent.class)
- .chatModel(chatLanguageModel)
- .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
- .build();
- }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/xly/config/MvcConfig.java b/src/main/java/com/xly/config/MvcConfig.java
index 53658e9..56f6c43 100644
--- a/src/main/java/com/xly/config/MvcConfig.java
+++ b/src/main/java/com/xly/config/MvcConfig.java
@@ -9,8 +9,7 @@ public class MvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
- // 直接映射URL到视图
registry.addViewController("/chat").setViewName("chat");
- registry.addViewController("/").setViewName("index");
+ registry.addRedirectViewController("/", "/chat");
}
}
\ No newline at end of file
diff --git a/src/main/java/com/xly/config/OperableChatMemoryProvider.java b/src/main/java/com/xly/config/OperableChatMemoryProvider.java
deleted file mode 100644
index c7aa9bd..0000000
--- a/src/main/java/com/xly/config/OperableChatMemoryProvider.java
+++ /dev/null
@@ -1,259 +0,0 @@
-package com.xly.config;
-
-import dev.langchain4j.memory.ChatMemory;
-import dev.langchain4j.memory.chat.ChatMemoryProvider;
-import dev.langchain4j.memory.chat.MessageWindowChatMemory;
-import dev.langchain4j.data.message.ChatMessage;
-import org.springframework.stereotype.Component;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-/**
- * 可操作的ChatMemoryProvider:获取消息对象+清除记忆(指定/全量)+删除单条消息
- * 实现框架原生接口,无缝对接AiServices,线程安全适配生产环境
- */
-@Component
-public class OperableChatMemoryProvider implements ChatMemoryProvider {
- // 核心缓存:memoryId -> ChatMemory,保证一个会话/用户对应唯一记忆实例
- private final Map