From ee583b894e10457f48867752e5ffa032b1ce1cdb Mon Sep 17 00:00:00 2001 From: zichun <26684461+reporkey@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:40:01 +0800 Subject: [PATCH] skills: ASCII filenames (unzip mangles CJK names in deploy chain), name moved into file line 1; bench: simulate production anti-fab retry guard --- bench/README.md | 11 ++++++++--- bench/bench_ext.py | 22 ++++++++++++++++++---- src/main/java/com/xly/service/SkillService.java | 30 +++++++++++++++++++----------- src/main/resources/skills/query.md | 8 ++++++++ src/main/resources/skills/quote-create.md | 9 +++++++++ src/main/resources/skills/record-create.md | 7 +++++++ src/main/resources/skills/record-status.md | 14 ++++++++++++++ src/main/resources/skills/record-update.md | 8 ++++++++ src/main/resources/skills/修改记录.md | 7 ------- src/main/resources/skills/单据状态操作.md | 13 ------------- src/main/resources/skills/新建单据.md | 6 ------ src/main/resources/skills/新建报价.md | 8 -------- src/main/resources/skills/查询数据.md | 7 ------- 13 files changed, 91 insertions(+), 59 deletions(-) create mode 100644 src/main/resources/skills/query.md create mode 100644 src/main/resources/skills/quote-create.md create mode 100644 src/main/resources/skills/record-create.md create mode 100644 src/main/resources/skills/record-status.md create mode 100644 src/main/resources/skills/record-update.md delete mode 100644 src/main/resources/skills/修改记录.md delete mode 100644 src/main/resources/skills/单据状态操作.md delete mode 100644 src/main/resources/skills/新建单据.md delete mode 100644 src/main/resources/skills/新建报价.md delete mode 100644 src/main/resources/skills/查询数据.md 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/query.md b/src/main/resources/skills/query.md new file mode 100644 index 0000000..481f2d4 --- /dev/null +++ b/src/main/resources/skills/query.md @@ -0,0 +1,8 @@ +查询数据 +查数量/概况、找记录、看单条详情、翻页时的标准查法 +【技能:查询数据】 +1. 不确定该查哪张表单:先 findForms(业务关键词);同名多张时优先选检索结果靠前的那张。 +2. 问总数/概况:readFormData 且 keyword 留空;找某个名称的记录:readFormData 填 keyword;要某条记录的完整信息或某个字段(电话/销售员…):lookupRecord。 +3. 用户要下一页/更多:readFormData 同参数、page 加 1。 +4. 答案里的每个数字都必须来自本轮工具结果;工具没查到就如实说没查到。 +5. 回答用记录名称,不用内部 id。给出答案即停。 diff --git a/src/main/resources/skills/quote-create.md b/src/main/resources/skills/quote-create.md new file mode 100644 index 0000000..7b88fbd --- /dev/null +++ b/src/main/resources/skills/quote-create.md @@ -0,0 +1,9 @@ +新建报价 +用户要报价、或问定制产品价格(多少钱/什么价/报个价)——价格由核价算出,系统里没有现成价 +【技能:新建报价】 +问价 = 新建报价单:系统里没有现成价格,价格由 ERP 核价算出。只有给了单号、或明确说查已有报价,才是查询。 +1. collectForm("报价", knownFieldsJson):把用户已说的信息作为 knownFieldsJson 预填;值**逐字照抄原话**(「大16开」就写「大16开」,不得改写)。要报价的物品(纸盒/彩盒/画册…)是**产品**,出钱的公司名才是**客户**。 +2. 表单弹出后本轮结束:提示用户在表单里补齐(客户/产品从下拉里选真实数据)并点【提交】,停下等待。 +3. 用户提交表单后(消息形如「提交「报价」新增表单:…」):proposeWrite(action=create, entityKeyword=报价, fieldsJson=表单字段)。 +4. 提议卡出现即停:提示用户点【确认】。确认前什么都没写入,绝不说「已生成/已完成」。 +5. 用户问价格数字:请他确认后在 ERP 里点核价得到;你自己不报任何价格数字。 diff --git a/src/main/resources/skills/record-create.md b/src/main/resources/skills/record-create.md new file mode 100644 index 0000000..d7a8bed --- /dev/null +++ b/src/main/resources/skills/record-create.md @@ -0,0 +1,7 @@ +新建单据 +新建报价以外的单据或资料(客户/供应商/物料/订单…) +【技能:新建单据】 +1. 单据类型不明确:askUser 问一次要新建哪种单据,停下等回答。 +2. collectForm(单据类型, knownFieldsJson):把用户已说的信息预填,值逐字照抄原话。 +3. 角色对照:出钱的公司=客户;要生产/加工/报价的物品=产品。引用到的客户/产品/物料必须是系统里已存在的真实记录;找不到就请用户核对名称或从下拉里选,绝不代建。 +4. 表单弹出即停,等用户提交;提交后 proposeWrite(action=create);提议卡出现即停,请用户点【确认】,绝不说已完成。 diff --git a/src/main/resources/skills/record-status.md b/src/main/resources/skills/record-status.md new file mode 100644 index 0000000..4351b86 --- /dev/null +++ b/src/main/resources/skills/record-status.md @@ -0,0 +1,14 @@ +单据状态操作 +作废/删除、复原、审核、反审核/销审某张已有单据 +【技能:单据状态操作】 +动作对照(proposeWrite 的 action): +- 删除/取消/不要了 → invalid(作废,可复原) +- 复原/恢复/取消作废 → cancelInvalid +- 审核/审核通过 → examine +- 反审核/销审/撤回审核 → cancelExamine +- 用户明说「物理删除/彻底删除」→ delete,并提醒不可恢复 +1. 确认操作哪条记录(名称/单号);上下文里定位不到就 askUser 一次,停下等回答。 +2. proposeWrite(action=对照表动作, entityKeyword=单据类型, recordKeyword=记录名或单号)。 +3. 工具提示多条匹配:念候选让用户选后再调一次。 +4. 提议卡出现即停,请用户点【确认】;绝不说已完成。 +消歧:改「审核人」等名字带“审核”的字段 → 用【修改记录】技能(update),不是本技能。 diff --git a/src/main/resources/skills/record-update.md b/src/main/resources/skills/record-update.md new file mode 100644 index 0000000..5dd44ed --- /dev/null +++ b/src/main/resources/skills/record-update.md @@ -0,0 +1,8 @@ +修改记录 +改某条已有记录的某个字段——含改「审核人」这类名字带“审核”的字段 +【技能:修改记录】 +1. 需要三样:哪条记录(名称/单号)、改哪个字段(中文名)、新值。缺哪样就 askUser **一次**把缺的问全,停下等回答。 +2. 齐了就 proposeWrite(action=update, entityKeyword=实体类型, recordKeyword=记录名或单号, fieldChinese=字段中文名, newValue=新值);newValue 逐字照抄用户的话。 +3. 工具提示多条匹配:把候选念给用户选,选定后再调一次。 +4. 提议卡出现即停,请用户点【确认】;绝不说已完成。 +注意:改「审核人」「复审人」这类**名字带“审核”的字段**是修改记录(update),不是审核操作。 diff --git a/src/main/resources/skills/修改记录.md b/src/main/resources/skills/修改记录.md deleted file mode 100644 index c9c869e..0000000 --- a/src/main/resources/skills/修改记录.md +++ /dev/null @@ -1,7 +0,0 @@ -改某条已有记录的某个字段——含改「审核人」这类名字带“审核”的字段 -【技能:修改记录】 -1. 需要三样:哪条记录(名称/单号)、改哪个字段(中文名)、新值。缺哪样就 askUser **一次**把缺的问全,停下等回答。 -2. 齐了就 proposeWrite(action=update, entityKeyword=实体类型, recordKeyword=记录名或单号, fieldChinese=字段中文名, newValue=新值);newValue 逐字照抄用户的话。 -3. 工具提示多条匹配:把候选念给用户选,选定后再调一次。 -4. 提议卡出现即停,请用户点【确认】;绝不说已完成。 -注意:改「审核人」「复审人」这类**名字带“审核”的字段**是修改记录(update),不是审核操作。 diff --git a/src/main/resources/skills/单据状态操作.md b/src/main/resources/skills/单据状态操作.md deleted file mode 100644 index dbf0543..0000000 --- a/src/main/resources/skills/单据状态操作.md +++ /dev/null @@ -1,13 +0,0 @@ -作废/删除、复原、审核、反审核/销审某张已有单据 -【技能:单据状态操作】 -动作对照(proposeWrite 的 action): -- 删除/取消/不要了 → invalid(作废,可复原) -- 复原/恢复/取消作废 → cancelInvalid -- 审核/审核通过 → examine -- 反审核/销审/撤回审核 → cancelExamine -- 用户明说「物理删除/彻底删除」→ delete,并提醒不可恢复 -1. 确认操作哪条记录(名称/单号);上下文里定位不到就 askUser 一次,停下等回答。 -2. proposeWrite(action=对照表动作, entityKeyword=单据类型, recordKeyword=记录名或单号)。 -3. 工具提示多条匹配:念候选让用户选后再调一次。 -4. 提议卡出现即停,请用户点【确认】;绝不说已完成。 -消歧:改「审核人」等名字带“审核”的字段 → 用【修改记录】技能(update),不是本技能。 diff --git a/src/main/resources/skills/新建单据.md b/src/main/resources/skills/新建单据.md deleted file mode 100644 index 943112b..0000000 --- a/src/main/resources/skills/新建单据.md +++ /dev/null @@ -1,6 +0,0 @@ -新建报价以外的单据或资料(客户/供应商/物料/订单…) -【技能:新建单据】 -1. 单据类型不明确:askUser 问一次要新建哪种单据,停下等回答。 -2. collectForm(单据类型, knownFieldsJson):把用户已说的信息预填,值逐字照抄原话。 -3. 角色对照:出钱的公司=客户;要生产/加工/报价的物品=产品。引用到的客户/产品/物料必须是系统里已存在的真实记录;找不到就请用户核对名称或从下拉里选,绝不代建。 -4. 表单弹出即停,等用户提交;提交后 proposeWrite(action=create);提议卡出现即停,请用户点【确认】,绝不说已完成。 diff --git a/src/main/resources/skills/新建报价.md b/src/main/resources/skills/新建报价.md deleted file mode 100644 index 9bd1fbf..0000000 --- a/src/main/resources/skills/新建报价.md +++ /dev/null @@ -1,8 +0,0 @@ -用户要报价、或问定制产品价格(多少钱/什么价/报个价)——价格由核价算出,系统里没有现成价 -【技能:新建报价】 -问价 = 新建报价单:系统里没有现成价格,价格由 ERP 核价算出。只有给了单号、或明确说查已有报价,才是查询。 -1. collectForm("报价", knownFieldsJson):把用户已说的信息作为 knownFieldsJson 预填;值**逐字照抄原话**(「大16开」就写「大16开」,不得改写)。要报价的物品(纸盒/彩盒/画册…)是**产品**,出钱的公司名才是**客户**。 -2. 表单弹出后本轮结束:提示用户在表单里补齐(客户/产品从下拉里选真实数据)并点【提交】,停下等待。 -3. 用户提交表单后(消息形如「提交「报价」新增表单:…」):proposeWrite(action=create, entityKeyword=报价, fieldsJson=表单字段)。 -4. 提议卡出现即停:提示用户点【确认】。确认前什么都没写入,绝不说「已生成/已完成」。 -5. 用户问价格数字:请他确认后在 ERP 里点核价得到;你自己不报任何价格数字。 diff --git a/src/main/resources/skills/查询数据.md b/src/main/resources/skills/查询数据.md deleted file mode 100644 index 10aae88..0000000 --- a/src/main/resources/skills/查询数据.md +++ /dev/null @@ -1,7 +0,0 @@ -查数量/概况、找记录、看单条详情、翻页时的标准查法 -【技能:查询数据】 -1. 不确定该查哪张表单:先 findForms(业务关键词);同名多张时优先选检索结果靠前的那张。 -2. 问总数/概况:readFormData 且 keyword 留空;找某个名称的记录:readFormData 填 keyword;要某条记录的完整信息或某个字段(电话/销售员…):lookupRecord。 -3. 用户要下一页/更多:readFormData 同参数、page 加 1。 -4. 答案里的每个数字都必须来自本轮工具结果;工具没查到就如实说没查到。 -5. 回答用记录名称,不用内部 id。给出答案即停。 -- libgit2 0.22.2