This commit is contained in:
hupenglong1
2026-05-22 20:56:57 +08:00
parent 5311e6d97c
commit 362321c460
3 changed files with 164 additions and 19 deletions
+6
View File
@@ -6128,6 +6128,9 @@ def _build_pipeline_items(
# amber pill ("等待人工确认") instead of the spinning blue "IN PROGRESS"
# used for actual running steps.
gate_status = 'waiting' if raw_status == 'running' else raw_status
# Structured fields for the gate body — agent should prefer these so
# the UI can render labeled rows (现状 / 提议 / 请选). `reason` is kept
# as a free-text fallback for entries that haven't been migrated.
gate_items.append({
'type': 'gate',
'key': gate_key,
@@ -6138,6 +6141,9 @@ def _build_pipeline_items(
'description': meta_g['description'],
'status': gate_status,
'reason': entry.get('reason'),
'summary': entry.get('summary'),
'proposal': entry.get('proposal'),
'ask': entry.get('ask'),
'_order_key': (run_index, 99, gate_position_index.get(gate_key, 0)),
})
@@ -68,6 +68,9 @@ type GateItem = {
description: string;
status: CardStatus;
reason?: string | null;
summary?: string | null;
proposal?: string | null;
ask?: string | null;
};
type PipelineItem = RoundItem | GateItem;
@@ -485,6 +488,95 @@ function CardArrow() {
);
}
function parseGateReason(reason: string | null | undefined): {
summary: string | null;
proposal: string | null;
ask: string | null;
} {
const text = (reason ?? "").trim();
if (!text) return { summary: null, proposal: null, ask: null };
// Find the ask: greedy from any "是否/要不要/能否/还是/请..." question phrase
// to end. Fallback: the trailing sentence containing ?/.
const askPatterns = [
/(是否[^。]*$)/,
/(要不要[^。]*$)/,
/(能否[^。]*$)/,
/(请[^。]{0,40}[?][^。]*$)/,
];
let ask: string | null = null;
let body = text;
for (const pat of askPatterns) {
const m = text.match(pat);
if (m && m.index !== undefined) {
ask = m[0].trim().replace(/^[。;,\s]+/, "");
body = text.slice(0, m.index).trim().replace(/[。;,\s]+$/, "");
break;
}
}
if (!ask) {
// Fallback: split off trailing sentence(s) containing ? or
const qIdx = Math.max(text.lastIndexOf("?"), text.lastIndexOf(""));
if (qIdx >= 0) {
// expand back to previous 。 or start of string
const prevDot = Math.max(
text.lastIndexOf("。", qIdx - 1),
text.lastIndexOf(";", qIdx - 1),
text.lastIndexOf("", qIdx - 1),
);
ask = text.slice(prevDot + 1).trim().replace(/^[。;,\s]+/, "");
body = text.slice(0, prevDot + 1).trim().replace(/[。;,\s]+$/, "");
}
}
const sentences = body
.split(/。/)
.map((s) => s.trim())
.filter(Boolean);
if (sentences.length === 0) {
return { summary: null, proposal: null, ask };
}
if (sentences.length === 1) {
return { summary: sentences[0], proposal: null, ask };
}
const proposalKw =
/(H\d|H[1-3]\b|拟|要做|计划|提议|方案|增强|仿写|修改|修\s*\d|fix|patch|候选|逐条审|relabel|sft|train)/i;
let splitIdx = sentences.findIndex((s) => proposalKw.test(s));
if (splitIdx <= 0) splitIdx = Math.max(1, Math.floor(sentences.length / 2));
const summary = sentences.slice(0, splitIdx).join("。 ");
const proposal = sentences.slice(splitIdx).join("。 ");
return { summary, proposal, ask };
}
function GateBodyRow({
label,
text,
tone,
}: {
label: string;
text: string;
tone: "status" | "proposal" | "ask";
}) {
const labelTone =
tone === "ask"
? "bg-amber-500/25 text-amber-800 dark:bg-amber-400/30 dark:text-amber-100"
: tone === "proposal"
? "bg-sky-500/15 text-sky-700 dark:bg-sky-400/20 dark:text-sky-200"
: "bg-zinc-500/15 text-zinc-700 dark:bg-zinc-400/20 dark:text-zinc-200";
const textTone = tone === "ask" ? "text-foreground font-medium" : "text-foreground/85";
return (
<div className="flex items-start gap-2">
<span
className={cn(
"shrink-0 rounded px-1.5 py-0.5 font-mono text-[10px] font-semibold tracking-wider",
labelTone,
)}
>
{label}
</span>
<p className={cn("text-sm leading-relaxed", textTone)}>{text}</p>
</div>
);
}
function GateCard({
item,
onClick,
@@ -500,12 +592,18 @@ function GateCard({
item.gate_kind === "human-review"
? "在右侧对话里给出 OK / 调整后的指令,agent 才会继续。"
: "在右侧对话里回 OK / 修改建议,确认后进入下一轮训练。";
// Prefer structured fields; fall back to heuristic parsing of `reason`.
const parsed = parseGateReason(item.reason);
const summary = item.summary ?? parsed.summary;
const proposal = item.proposal ?? parsed.proposal;
const ask = item.ask ?? parsed.ask;
const hasStructured = Boolean(summary || proposal || ask);
return (
<Tag
type={onClick ? "button" : undefined}
onClick={onClick}
className={cn(
"flex w-full items-center justify-between gap-3 rounded-2xl border-2 border-dashed bg-amber-500/5 p-4 text-left transition-colors",
"flex w-full items-center justify-between gap-4 rounded-2xl border-2 border-dashed bg-amber-500/5 p-5 text-left transition-colors",
isWaiting
? "border-amber-400 bg-amber-500/10 ring-2 ring-amber-300/40 dark:ring-amber-500/30 animate-pipeline-pill-glow"
: isComplete
@@ -516,32 +614,42 @@ function GateCard({
)}
>
<div className="flex items-center gap-3 min-w-0">
<span className="flex size-9 shrink-0 items-center justify-center rounded-full bg-amber-500/15 font-mono text-amber-700 text-sm font-semibold dark:text-amber-300">
<span className="flex size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/15 font-mono text-amber-700 text-base font-semibold dark:text-amber-300">
{numLabel}
</span>
<div className="flex size-10 shrink-0 items-center justify-center rounded-full bg-amber-500/20 text-amber-700 dark:text-amber-300">
<UserCheckIcon className="size-5" />
</div>
<div className="min-w-0">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-foreground text-sm">
<UserCheckIcon className="size-5 shrink-0 text-amber-700 dark:text-amber-300" />
<span className="font-semibold text-foreground text-base">
{item.title}
</span>
<span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-[10px] text-amber-700 dark:text-amber-300">
<span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-xs text-amber-700 dark:text-amber-300">
{item.run_id}
</span>
</div>
{item.reason ? (
<p className="mt-1 text-[12px] text-foreground/80 leading-snug line-clamp-3">
{hasStructured ? (
<div className="mt-2 space-y-1.5">
{summary ? (
<GateBodyRow label="现状" text={summary} tone="status" />
) : null}
{proposal ? (
<GateBodyRow label="提议" text={proposal} tone="proposal" />
) : null}
{ask ? (
<GateBodyRow label="请选" text={ask} tone="ask" />
) : null}
</div>
) : item.reason ? (
<p className="mt-1.5 text-sm text-foreground/85 leading-relaxed">
{item.reason}
</p>
) : (
<p className="mt-1 text-[11px] text-muted-foreground leading-snug">
<p className="mt-1.5 text-sm text-muted-foreground leading-relaxed">
{item.description}
</p>
)}
{isWaiting ? (
<p className="mt-1 text-[10px] uppercase tracking-wide text-amber-700/80 dark:text-amber-400/80">
<p className="mt-2 text-xs tracking-wide text-amber-700/90 dark:text-amber-400/90">
{hint}
</p>
) : null}
+38 -7
View File
@@ -444,15 +444,25 @@ assets/
#### 写法
**强烈推荐结构化三段写法**`summary` / `proposal` / `ask`)——UI 会渲染成"现状 / 提议 / 请选"三个带标签行,用户一眼看明白。`reason` 留作 fallback。
```bash
# R0 baseline 之后想让用户拍板是否进 train
echo '{"step":"human-check","status":"running","run_id":"R0","reason":"R0 baseline 分析完成,目标子集 X.X% 偏低,确认是否按当前 H1 候选进 R1 train","ts":"'$(date -Iseconds)'"}' >> $S
# R0 baseline 之后想让用户拍板是否进 train(推荐结构化)
echo '{"step":"human-check","status":"running","run_id":"R0",
"summary":"R0 baseline 完成;复杂导航过召专项0511 = 58.89%(53/90)37 错全为 complex 误判",
"proposal":"H1(标签纠错):把 7 条原标 complex=true 但实际是简单导航的样本改回 complex=false。 H2(数据增强):仿写 100 条 complex=false 的简单导航 query 补进训练集,平衡正负样本",
"ask":"是否按 H1+H2 一起进 R1 train?还是先只跑 H1 验证一轮?",
"ts":"'$(date -Iseconds)'"}' >> $S
# R1 评测发现 regression,需要用户决定是否回滚
echo '{"step":"human-review","status":"running","run_id":"R1","reason":"R1 vs R0:导航bvt -3.2pp, 可聊可控 -25pp,建议回滚","ts":"'$(date -Iseconds)'"}' >> $S
# R1 评测发现 regression
echo '{"step":"human-review","status":"running","run_id":"R1",
"summary":"R1 vs R0:导航bvt -3.2pp,可聊可控 -25pp,目标子集 +1.4pp",
"proposal":"回滚到 R0 权重;下一轮把 H2 仿写量减半,避免对 complex=false 过拟合",
"ask":"回滚 R0 还是继续跑 R2 看曲线?",
"ts":"'$(date -Iseconds)'"}' >> $S
# 候选量超 200 条命中触发条件 #3
echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量 412 命中触发条件 #3","ts":"'$(date -Iseconds)'"}' >> $S
# fallback:只写 reason 也能跑(前端会启发式切分),但不如结构化清晰
echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量 412 命中规则上限,需要人工定更精细 pattern 收窄","ts":"'$(date -Iseconds)'"}' >> $S
```
**顺序不能反**:先 echo gate entry → 再 chat reply 给用户。否则用户先看到聊天问话、UI 里却没卡,会困惑"流程是不是卡死了"。
@@ -464,9 +474,30 @@ echo '{"step":"human-check","status":"running","run_id":"R0","reason":"候选量
| `step` | ✅ | `human-check``human-review` |
| `status` | ✅ | 卡进入时写 `running`;用户回复后写 `complete` |
| `run_id` | ✅ | 当前所在轮次(决定 gate 插在哪个 section 后) |
| `reason` | | 触发原因——直接展示给用户看,写具体一些("Gold drift 12 条,触发 #1" 比 "需要人审" 强得多) |
| `summary` | 🔼 | **现状一句话**:跑了什么、关键数字。例:`R0 baseline 完成;专项 58.89%(53/90)37 错全为 complex 误判` |
| `proposal` | 🔼 | **打算怎么干**:每个 H 单独说"H? (类型):具体做什么"。详见下面规则 |
| `ask` | 🔼 | **让用户选什么**:用问句给出 A/B 选项。例:`是否按 H1+H2 进 R1 train?还是先只跑 H1 验证一轮?` |
| `reason` | ⭕ | 兜底用:没写 summary/proposal/ask 时前端会拿 reason 做启发式切分。但**优先用结构化三段**,别只写 reason |
| `ts` | ✅ | ISO 时间戳 |
🔼 = 强烈推荐写——三段都填 UI 会变成清晰的"现状/提议/请选"分块;都不填只填 reason 也能跑,但用户要自己抠语义。
#### proposal 写法规则(关键)
**每个假设单独一句,结构 = `H? (一两个字概括类型):具体动作 + 数量 + 目标`**。比如:
-`H1(标签纠错):把 7 条原标 complex=true 但实际是简单导航的样本改回 complex=false`
-`H2(数据增强):仿写 100 条 complex=false 的简单导航 query 补进训练集`
-`H1 修 7 条 mislabeled` ← 用户要猜 mislabeled 是什么意思
-`H2 仿写 100 条 complex=false 简单导航` ← 没说补到哪、为啥补
**不要写进 proposal 的内容**
- §X.X.X 规则引用(`触发 §4.0.1 Step B``命中条件 #3`)—— 用户不关心你按哪条规则做的,只关心你要做什么
- 流程自洽说明(`需要人工逐条审 1/0``走 sanity check``过 label-master 复核`)—— 这些是 agent 内部流程,对用户决策没用
- 候选量区间括号注释(`100~150 触发 51-200 区间`)—— 数量 OK,区间归属归属是规则细节,删
写 proposal 时问自己:**"这句话如果交给一个新加入的产品同学看,他能不能 5 秒内明白要干什么"**。能 = ✅;得回头查 §X.X.X 才能懂 = ❌,重写。
**用户拍板回复后**append `status:"complete"` 同 step + run_id 一行,gate 卡变 COMPLETED;然后才继续往下走(继续/回滚/调参)。
**两种 gate 的区分(约定)**