Commit ee583b894e10457f48867752e5ffa032b1ce1cdb

Authored by zichun
1 parent b7543403

skills: ASCII filenames (unzip mangles CJK names in deploy chain), name moved in…

…to file line 1; bench: simulate production anti-fab retry guard
bench/README.md
@@ -24,11 +24,16 @@ skill-ReAct 落地后:意图门段作废;agent 段对新架构重跑,严 @@ -24,11 +24,16 @@ skill-ReAct 落地后:意图门段作废;agent 段对新架构重跑,严
24 | 提议纪律 | M7 | 提议卡后不重复提议、不声称已执行 | 24 | 提议纪律 | M7 | 提议卡后不重复提议、不声称已执行 |
25 25
26 ``` 26 ```
27 -uv run --no-project bench_ext.py --arch old # 现产线复刻(基线)  
28 -uv run --no-project bench_ext.py --arch new # skill-ReAct(从 src/main/resources/prompts/ 读) 27 +uv run --no-project bench_ext.py --arch old # 旧架构复刻(基线)
  28 +uv run --no-project bench_ext.py --arch new # skill-ReAct(prompt/skill 从 src/main/resources/ 同源读取)
  29 +uv run --no-project bench_ext.py --fifty --arch new # bench50 的 50 题按全轨迹判分(新旧同口径对照)
29 uv run --no-project bench_ext.py --arch old --only M5 30 uv run --no-project bench_ext.py --arch old --only M5
30 ``` 31 ```
31 32
32 -老架构基线存档:`bench_ext_old_baseline.out`。 33 +轨迹模拟含**生产反编造护栏**复刻:零工具调用却答出数字 → 注入纠正话术重试一次
  34 +(与 AgentChatController 的常开护栏一致;不模拟 WRITE_CLAIM 文案提醒)。
  35 +
  36 +基线存档:`bench_ext_old_baseline.out`(无护栏口径,首次基线)、`bench_p3_acceptance.out`(P3 验收,
  37 +四组同口径:ext/fifty × old/new)。
33 38
34 流式输出中途点【确认】的并发问题由 Java 单测覆盖(事件日志并发 append 不丢事件),不在本目录。 39 流式输出中途点【确认】的并发问题由 Java 单测覆盖(事件日志并发 append 不丢事件),不在本目录。
bench/bench_ext.py
@@ -41,11 +41,12 @@ T_USESKILL = fn("useSkill", @@ -41,11 +41,12 @@ T_USESKILL = fn("useSkill",
41 41
42 42
43 def load_skills(): 43 def load_skills():
44 - """{名称: (一句话用途, 正文)} —— 与生产 SkillService 同源读仓库 skill 文件。""" 44 + """{名称: (一句话用途, 正文)} —— 与生产 SkillService 同源同格式(首行=名,次行=用途,余=正文)。"""
45 out = {} 45 out = {}
46 for p in sorted(SKILLS_DIR.glob("*.md")): 46 for p in sorted(SKILLS_DIR.glob("*.md")):
47 - lines = p.read_text(encoding="utf-8").split("\n", 1)  
48 - out[p.stem] = (lines[0].strip(), lines[1].strip() if len(lines) > 1 else "") 47 + parts = p.read_text(encoding="utf-8").split("\n", 2)
  48 + if len(parts) >= 3:
  49 + out[parts[0].strip()] = (parts[1].strip(), parts[2].strip())
49 return out 50 return out
50 51
51 52
@@ -136,7 +137,7 @@ def call(body, timeout=200): @@ -136,7 +137,7 @@ def call(body, timeout=200):
136 return json.loads(r.read()) 137 return json.loads(r.read())
137 138
138 139
139 -def run_trajectory(system_prompt, tools, history, utterance): 140 +def run_trajectory_once(system_prompt, tools, history, utterance):
140 """history: [(role, text)] 已渲染的往轮。返回 (calls, final_text, steps, err)。""" 141 """history: [(role, text)] 已渲染的往轮。返回 (calls, final_text, steps, err)。"""
141 messages = [{"role": "system", "content": system_prompt}] 142 messages = [{"role": "system", "content": system_prompt}]
142 for role, text in history: 143 for role, text in history:
@@ -168,6 +169,19 @@ def run_trajectory(system_prompt, tools, history, utterance): @@ -168,6 +169,19 @@ def run_trajectory(system_prompt, tools, history, utterance):
168 return calls, None, MAX_STEPS, "step-cap" 169 return calls, None, MAX_STEPS, "step-cap"
169 170
170 171
  172 +GUARD_NUDGE = "你上一条回答没有调用任何工具、数字疑似编造。请先用工具查询真实数据,再重新回答这个问题:"
  173 +
  174 +
  175 +def run_trajectory(system_prompt, tools, history, utterance):
  176 + """带生产反编造护栏的轨迹:零工具却答出数字 → 注入纠正话术重试一次(复刻 AgentChatController)。"""
  177 + calls, text, steps, err = run_trajectory_once(system_prompt, tools, history, utterance)
  178 + if err or calls or not text or not any(c.isdigit() for c in text):
  179 + return calls, text, steps, err
  180 + retry_hist = history + [("user", utterance), ("assistant", text)]
  181 + calls2, text2, steps2, err2 = run_trajectory_once(system_prompt, tools, retry_hist, GUARD_NUDGE + utterance)
  182 + return calls2, text2, steps + steps2, err2
  183 +
  184 +
171 # ---------------- 判分辅助 ---------------- 185 # ---------------- 判分辅助 ----------------
172 def argstr(args): 186 def argstr(args):
173 return json.dumps(args or {}, ensure_ascii=False) 187 return json.dumps(args or {}, ensure_ascii=False)
src/main/java/com/xly/service/SkillService.java
@@ -18,9 +18,10 @@ import java.util.stream.Stream; @@ -18,9 +18,10 @@ import java.util.stream.Stream;
18 /** 18 /**
19 * skill 存储 —— 业务流程知识做成文本技能,模型用 useSkill 载入后照步骤执行。 19 * skill 存储 —— 业务流程知识做成文本技能,模型用 useSkill 载入后照步骤执行。
20 * 20 *
21 - * <p>文件格式:{@code <名称>.md},首行 = 一句话用途(进 system prompt 索引),其余 = 技能正文。  
22 - * 默认从 classpath {@code skills/*.md} 读取(随包发布、缓存一次);配置  
23 - * {@code xly.skills.dir} 指向文件系统目录后改从该目录**每次现读**——改文件即热更,不用重启。 21 + * <p>文件格式:{@code skills/*.md}(文件名用 ASCII——部署链路里 unzip war 会把中文文件名搞乱),
  22 + * 首行 = 技能名,次行 = 一句话用途(进 system prompt 索引),其余 = 技能正文。
  23 + * 默认从 classpath 读取(随包发布、缓存一次);配置 {@code xly.skills.dir} 指向文件系统目录后
  24 + * 改从该目录**每次现读**——改文件即热更,不用重启。
24 */ 25 */
25 @Service 26 @Service
26 public class SkillService { 27 public class SkillService {
@@ -80,7 +81,7 @@ public class SkillService { @@ -80,7 +81,7 @@ public class SkillService {
80 try (Stream<Path> files = Files.list(dir)) { 81 try (Stream<Path> files = Files.list(dir)) {
81 files.filter(p -> p.getFileName().toString().endsWith(".md")).sorted().forEach(p -> { 82 files.filter(p -> p.getFileName().toString().endsWith(".md")).sorted().forEach(p -> {
82 try { 83 try {
83 - Skill s = parse(p.getFileName().toString(), Files.readString(p, StandardCharsets.UTF_8)); 84 + Skill s = parse(Files.readString(p, StandardCharsets.UTF_8));
84 if (s != null) { 85 if (s != null) {
85 out.add(s); 86 out.add(s);
86 } 87 }
@@ -101,7 +102,7 @@ public class SkillService { @@ -101,7 +102,7 @@ public class SkillService {
101 .getResources("classpath:skills/*.md"); 102 .getResources("classpath:skills/*.md");
102 for (Resource r : resources) { 103 for (Resource r : resources) {
103 try { 104 try {
104 - Skill s = parse(r.getFilename(), new String( 105 + Skill s = parse(new String(
105 r.getInputStream().readAllBytes(), StandardCharsets.UTF_8)); 106 r.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
106 if (s != null) { 107 if (s != null) {
107 out.add(s); 108 out.add(s);
@@ -117,14 +118,21 @@ public class SkillService { @@ -117,14 +118,21 @@ public class SkillService {
117 return out; 118 return out;
118 } 119 }
119 120
120 - private static Skill parse(String filename, String content) {  
121 - if (filename == null || content == null || content.isBlank()) { 121 + /** 首行=技能名,次行=一句话用途,其余=正文。 */
  122 + private static Skill parse(String content) {
  123 + if (content == null || content.isBlank()) {
  124 + return null;
  125 + }
  126 + String[] parts = content.split("\n", 3);
  127 + if (parts.length < 3) {
  128 + return null;
  129 + }
  130 + String name = parts[0].trim();
  131 + String brief = parts[1].trim();
  132 + String body = parts[2].trim();
  133 + if (name.isEmpty() || body.isEmpty()) {
122 return null; 134 return null;
123 } 135 }
124 - String name = filename.endsWith(".md") ? filename.substring(0, filename.length() - 3) : filename;  
125 - int nl = content.indexOf('\n');  
126 - String brief = (nl < 0 ? content : content.substring(0, nl)).trim();  
127 - String body = nl < 0 ? "" : content.substring(nl + 1).trim();  
128 return new Skill(name, brief, body); 136 return new Skill(name, brief, body);
129 } 137 }
130 } 138 }
src/main/resources/skills/查询数据.md renamed to src/main/resources/skills/query.md
  1 +查询数据
1 查数量/概况、找记录、看单条详情、翻页时的标准查法 2 查数量/概况、找记录、看单条详情、翻页时的标准查法
2 【技能:查询数据】 3 【技能:查询数据】
3 1. 不确定该查哪张表单:先 findForms(业务关键词);同名多张时优先选检索结果靠前的那张。 4 1. 不确定该查哪张表单:先 findForms(业务关键词);同名多张时优先选检索结果靠前的那张。
src/main/resources/skills/新建报价.md renamed to src/main/resources/skills/quote-create.md
  1 +新建报价
1 用户要报价、或问定制产品价格(多少钱/什么价/报个价)——价格由核价算出,系统里没有现成价 2 用户要报价、或问定制产品价格(多少钱/什么价/报个价)——价格由核价算出,系统里没有现成价
2 【技能:新建报价】 3 【技能:新建报价】
3 问价 = 新建报价单:系统里没有现成价格,价格由 ERP 核价算出。只有给了单号、或明确说查已有报价,才是查询。 4 问价 = 新建报价单:系统里没有现成价格,价格由 ERP 核价算出。只有给了单号、或明确说查已有报价,才是查询。
src/main/resources/skills/新建单据.md renamed to src/main/resources/skills/record-create.md
  1 +新建单据
1 新建报价以外的单据或资料(客户/供应商/物料/订单…) 2 新建报价以外的单据或资料(客户/供应商/物料/订单…)
2 【技能:新建单据】 3 【技能:新建单据】
3 1. 单据类型不明确:askUser 问一次要新建哪种单据,停下等回答。 4 1. 单据类型不明确:askUser 问一次要新建哪种单据,停下等回答。
src/main/resources/skills/单据状态操作.md renamed to src/main/resources/skills/record-status.md
  1 +单据状态操作
1 作废/删除、复原、审核、反审核/销审某张已有单据 2 作废/删除、复原、审核、反审核/销审某张已有单据
2 【技能:单据状态操作】 3 【技能:单据状态操作】
3 动作对照(proposeWrite 的 action): 4 动作对照(proposeWrite 的 action):
src/main/resources/skills/修改记录.md renamed to src/main/resources/skills/record-update.md
  1 +修改记录
1 改某条已有记录的某个字段——含改「审核人」这类名字带“审核”的字段 2 改某条已有记录的某个字段——含改「审核人」这类名字带“审核”的字段
2 【技能:修改记录】 3 【技能:修改记录】
3 1. 需要三样:哪条记录(名称/单号)、改哪个字段(中文名)、新值。缺哪样就 askUser **一次**把缺的问全,停下等回答。 4 1. 需要三样:哪条记录(名称/单号)、改哪个字段(中文名)、新值。缺哪样就 askUser **一次**把缺的问全,停下等回答。