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 | 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 | 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 | 39 | 流式输出中途点【确认】的并发问题由 Java 单测覆盖(事件日志并发 append 不丢事件),不在本目录。 | ... | ... |
bench/bench_ext.py
| ... | ... | @@ -41,11 +41,12 @@ T_USESKILL = fn("useSkill", |
| 41 | 41 | |
| 42 | 42 | |
| 43 | 43 | def load_skills(): |
| 44 | - """{名称: (一句话用途, 正文)} —— 与生产 SkillService 同源读仓库 skill 文件。""" | |
| 44 | + """{名称: (一句话用途, 正文)} —— 与生产 SkillService 同源同格式(首行=名,次行=用途,余=正文)。""" | |
| 45 | 45 | out = {} |
| 46 | 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 | 50 | return out |
| 50 | 51 | |
| 51 | 52 | |
| ... | ... | @@ -136,7 +137,7 @@ def call(body, timeout=200): |
| 136 | 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 | 141 | """history: [(role, text)] 已渲染的往轮。返回 (calls, final_text, steps, err)。""" |
| 141 | 142 | messages = [{"role": "system", "content": system_prompt}] |
| 142 | 143 | for role, text in history: |
| ... | ... | @@ -168,6 +169,19 @@ def run_trajectory(system_prompt, tools, history, utterance): |
| 168 | 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 | 186 | def argstr(args): |
| 173 | 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 | 18 | /** |
| 19 | 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 | 26 | @Service |
| 26 | 27 | public class SkillService { |
| ... | ... | @@ -80,7 +81,7 @@ public class SkillService { |
| 80 | 81 | try (Stream<Path> files = Files.list(dir)) { |
| 81 | 82 | files.filter(p -> p.getFileName().toString().endsWith(".md")).sorted().forEach(p -> { |
| 82 | 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 | 85 | if (s != null) { |
| 85 | 86 | out.add(s); |
| 86 | 87 | } |
| ... | ... | @@ -101,7 +102,7 @@ public class SkillService { |
| 101 | 102 | .getResources("classpath:skills/*.md"); |
| 102 | 103 | for (Resource r : resources) { |
| 103 | 104 | try { |
| 104 | - Skill s = parse(r.getFilename(), new String( | |
| 105 | + Skill s = parse(new String( | |
| 105 | 106 | r.getInputStream().readAllBytes(), StandardCharsets.UTF_8)); |
| 106 | 107 | if (s != null) { |
| 107 | 108 | out.add(s); |
| ... | ... | @@ -117,14 +118,21 @@ public class SkillService { |
| 117 | 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 | 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 | 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