Commit ee583b894e10457f48867752e5ffa032b1ce1cdb
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
Showing
8 changed files
with
50 additions
and
18 deletions
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
src/main/resources/skills/新建报价.md renamed to src/main/resources/skills/quote-create.md
src/main/resources/skills/新建单据.md renamed to src/main/resources/skills/record-create.md
src/main/resources/skills/单据状态操作.md renamed to src/main/resources/skills/record-status.md
src/main/resources/skills/修改记录.md renamed to src/main/resources/skills/record-update.md