diff --git a/bench/README.md b/bench/README.md index 5c0226a..8ad8992 100644 --- a/bench/README.md +++ b/bench/README.md @@ -24,11 +24,16 @@ skill-ReAct 落地后:意图门段作废;agent 段对新架构重跑,严 | 提议纪律 | M7 | 提议卡后不重复提议、不声称已执行 | ``` -uv run --no-project bench_ext.py --arch old # 现产线复刻(基线) -uv run --no-project bench_ext.py --arch new # skill-ReAct(从 src/main/resources/prompts/ 读) +uv run --no-project bench_ext.py --arch old # 旧架构复刻(基线) +uv run --no-project bench_ext.py --arch new # skill-ReAct(prompt/skill 从 src/main/resources/ 同源读取) +uv run --no-project bench_ext.py --fifty --arch new # bench50 的 50 题按全轨迹判分(新旧同口径对照) uv run --no-project bench_ext.py --arch old --only M5 ``` -老架构基线存档:`bench_ext_old_baseline.out`。 +轨迹模拟含**生产反编造护栏**复刻:零工具调用却答出数字 → 注入纠正话术重试一次 +(与 AgentChatController 的常开护栏一致;不模拟 WRITE_CLAIM 文案提醒)。 + +基线存档:`bench_ext_old_baseline.out`(无护栏口径,首次基线)、`bench_p3_acceptance.out`(P3 验收, +四组同口径:ext/fifty × old/new)。 流式输出中途点【确认】的并发问题由 Java 单测覆盖(事件日志并发 append 不丢事件),不在本目录。 diff --git a/bench/bench_ext.py b/bench/bench_ext.py index 85013a9..39bdc0e 100644 --- a/bench/bench_ext.py +++ b/bench/bench_ext.py @@ -41,11 +41,12 @@ T_USESKILL = fn("useSkill", def load_skills(): - """{名称: (一句话用途, 正文)} —— 与生产 SkillService 同源读仓库 skill 文件。""" + """{名称: (一句话用途, 正文)} —— 与生产 SkillService 同源同格式(首行=名,次行=用途,余=正文)。""" out = {} for p in sorted(SKILLS_DIR.glob("*.md")): - lines = p.read_text(encoding="utf-8").split("\n", 1) - out[p.stem] = (lines[0].strip(), lines[1].strip() if len(lines) > 1 else "") + parts = p.read_text(encoding="utf-8").split("\n", 2) + if len(parts) >= 3: + out[parts[0].strip()] = (parts[1].strip(), parts[2].strip()) return out @@ -136,7 +137,7 @@ def call(body, timeout=200): return json.loads(r.read()) -def run_trajectory(system_prompt, tools, history, utterance): +def run_trajectory_once(system_prompt, tools, history, utterance): """history: [(role, text)] 已渲染的往轮。返回 (calls, final_text, steps, err)。""" messages = [{"role": "system", "content": system_prompt}] for role, text in history: @@ -168,6 +169,19 @@ def run_trajectory(system_prompt, tools, history, utterance): return calls, None, MAX_STEPS, "step-cap" +GUARD_NUDGE = "你上一条回答没有调用任何工具、数字疑似编造。请先用工具查询真实数据,再重新回答这个问题:" + + +def run_trajectory(system_prompt, tools, history, utterance): + """带生产反编造护栏的轨迹:零工具却答出数字 → 注入纠正话术重试一次(复刻 AgentChatController)。""" + calls, text, steps, err = run_trajectory_once(system_prompt, tools, history, utterance) + if err or calls or not text or not any(c.isdigit() for c in text): + return calls, text, steps, err + retry_hist = history + [("user", utterance), ("assistant", text)] + calls2, text2, steps2, err2 = run_trajectory_once(system_prompt, tools, retry_hist, GUARD_NUDGE + utterance) + return calls2, text2, steps + steps2, err2 + + # ---------------- 判分辅助 ---------------- def argstr(args): return json.dumps(args or {}, ensure_ascii=False) diff --git a/src/main/java/com/xly/service/SkillService.java b/src/main/java/com/xly/service/SkillService.java index d4792ec..f4452c1 100644 --- a/src/main/java/com/xly/service/SkillService.java +++ b/src/main/java/com/xly/service/SkillService.java @@ -18,9 +18,10 @@ import java.util.stream.Stream; /** * skill 存储 —— 业务流程知识做成文本技能,模型用 useSkill 载入后照步骤执行。 * - *

文件格式:{@code <名称>.md},首行 = 一句话用途(进 system prompt 索引),其余 = 技能正文。 - * 默认从 classpath {@code skills/*.md} 读取(随包发布、缓存一次);配置 - * {@code xly.skills.dir} 指向文件系统目录后改从该目录**每次现读**——改文件即热更,不用重启。 + *

文件格式:{@code skills/*.md}(文件名用 ASCII——部署链路里 unzip war 会把中文文件名搞乱), + * 首行 = 技能名,次行 = 一句话用途(进 system prompt 索引),其余 = 技能正文。 + * 默认从 classpath 读取(随包发布、缓存一次);配置 {@code xly.skills.dir} 指向文件系统目录后 + * 改从该目录**每次现读**——改文件即热更,不用重启。 */ @Service public class SkillService { @@ -80,7 +81,7 @@ public class SkillService { try (Stream files = Files.list(dir)) { files.filter(p -> p.getFileName().toString().endsWith(".md")).sorted().forEach(p -> { try { - Skill s = parse(p.getFileName().toString(), Files.readString(p, StandardCharsets.UTF_8)); + Skill s = parse(Files.readString(p, StandardCharsets.UTF_8)); if (s != null) { out.add(s); } @@ -101,7 +102,7 @@ public class SkillService { .getResources("classpath:skills/*.md"); for (Resource r : resources) { try { - Skill s = parse(r.getFilename(), new String( + Skill s = parse(new String( r.getInputStream().readAllBytes(), StandardCharsets.UTF_8)); if (s != null) { out.add(s); @@ -117,14 +118,21 @@ public class SkillService { return out; } - private static Skill parse(String filename, String content) { - if (filename == null || content == null || content.isBlank()) { + /** 首行=技能名,次行=一句话用途,其余=正文。 */ + private static Skill parse(String content) { + if (content == null || content.isBlank()) { + return null; + } + String[] parts = content.split("\n", 3); + if (parts.length < 3) { + return null; + } + String name = parts[0].trim(); + String brief = parts[1].trim(); + String body = parts[2].trim(); + if (name.isEmpty() || body.isEmpty()) { return null; } - String name = filename.endsWith(".md") ? filename.substring(0, filename.length() - 3) : filename; - int nl = content.indexOf('\n'); - String brief = (nl < 0 ? content : content.substring(0, nl)).trim(); - String body = nl < 0 ? "" : content.substring(nl + 1).trim(); return new Skill(name, brief, body); } } diff --git a/src/main/resources/skills/查询数据.md b/src/main/resources/skills/query.md index 10aae88..481f2d4 100644 --- a/src/main/resources/skills/查询数据.md +++ b/src/main/resources/skills/query.md @@ -1,3 +1,4 @@ +查询数据 查数量/概况、找记录、看单条详情、翻页时的标准查法 【技能:查询数据】 1. 不确定该查哪张表单:先 findForms(业务关键词);同名多张时优先选检索结果靠前的那张。 diff --git a/src/main/resources/skills/新建报价.md b/src/main/resources/skills/quote-create.md index 9bd1fbf..7b88fbd 100644 --- a/src/main/resources/skills/新建报价.md +++ b/src/main/resources/skills/quote-create.md @@ -1,3 +1,4 @@ +新建报价 用户要报价、或问定制产品价格(多少钱/什么价/报个价)——价格由核价算出,系统里没有现成价 【技能:新建报价】 问价 = 新建报价单:系统里没有现成价格,价格由 ERP 核价算出。只有给了单号、或明确说查已有报价,才是查询。 diff --git a/src/main/resources/skills/新建单据.md b/src/main/resources/skills/record-create.md index 943112b..d7a8bed 100644 --- a/src/main/resources/skills/新建单据.md +++ b/src/main/resources/skills/record-create.md @@ -1,3 +1,4 @@ +新建单据 新建报价以外的单据或资料(客户/供应商/物料/订单…) 【技能:新建单据】 1. 单据类型不明确:askUser 问一次要新建哪种单据,停下等回答。 diff --git a/src/main/resources/skills/单据状态操作.md b/src/main/resources/skills/record-status.md index dbf0543..4351b86 100644 --- a/src/main/resources/skills/单据状态操作.md +++ b/src/main/resources/skills/record-status.md @@ -1,3 +1,4 @@ +单据状态操作 作废/删除、复原、审核、反审核/销审某张已有单据 【技能:单据状态操作】 动作对照(proposeWrite 的 action): diff --git a/src/main/resources/skills/修改记录.md b/src/main/resources/skills/record-update.md index c9c869e..5dd44ed 100644 --- a/src/main/resources/skills/修改记录.md +++ b/src/main/resources/skills/record-update.md @@ -1,3 +1,4 @@ +修改记录 改某条已有记录的某个字段——含改「审核人」这类名字带“审核”的字段 【技能:修改记录】 1. 需要三样:哪条记录(名称/单号)、改哪个字段(中文名)、新值。缺哪样就 askUser **一次**把缺的问全,停下等回答。