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