判据 ⑨⑩ 收口:确定性蒸馏 —— 零 LLM 也留得下事实

终态封卷的蒸馏钩子原先「模型不可用就整次返回空」,活动记忆一条也留不下。
按「结构化部分零 LLM,叙述是视图不是事实」拆成两段:

  · 先落**确定性骨架**:这一单确实发生了什么 —— 终态、几步转了人兜底、
    放行被驳回几次 —— 全写在归侧条目里,不需要模型来讲。每条带证据链。
  · 模型可用时在其上补模式层 / 裁决层的归纳;归纳不出来也不丢掉整次蒸馏。

实测(A1 一单):3 条 fact 候选,各带证据链。

至此 MVP 十条判据:① ② ③ ⑥ ⑦ ⑧ ⑨ ⑩ 在真实系统上端到端跑通;
④ ⑤ 的 loop 机制(CMMN 装载 / Enabled 集 / 断点判定 / 回路调人)跑通,
余下部分受限于 LLM 端点 —— loop 的 Agent 每一轮决断都要模型。

gates: tsc / eslint / depcruise(317 模块)/ api-report / 402 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 05:58:43 +08:00
co-authored by Claude Opus 5
parent c3cb82ef64
commit bfe7828380
8 changed files with 156 additions and 25 deletions
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
import {
assertAppendOnly,
deriveSide,
deterministicDistill,
distillable,
ENTRY_KINDS,
redactForReader,
@@ -204,3 +205,27 @@ describe('归侧:治理干预', () => {
expect(lines.map((l) => l.sideLabel)).toEqual(['人裁决', '机器产出']);
});
});
describe('确定性蒸馏 —— 零 LLM 也留得下事实', () => {
it('终态、人兜底、驳回各归纳一条,且都带证据链', () => {
const drafts = deterministicDistill([
entry('terminal', { seq: 1, title: '终态:released' }),
entry('card-sent', { seq: 2, title: '步「S3 归一」重试仍败,转人兜底' }),
entry('release-verdict', { seq: 3, title: '放行裁决:redo' }),
]);
expect(drafts.map((d) => d.layer)).toEqual(['fact', 'fact', 'fact']);
expect(drafts.every((d) => d.entryIds.length > 0)).toBe(true);
expect(drafts[1]?.statement).toContain('转人兜底');
});
it('不可学习 / 已脱敏的条目不入蒸馏', () => {
const drafts = deterministicDistill([
entry('terminal', { seq: 1, governance: { masked: false, learnable: false } }),
]);
expect(drafts).toEqual([]);
});
it('无可蒸馏来源时返回空,不硬造条目', () => {
expect(deterministicDistill([])).toEqual([]);
});
});
@@ -116,3 +116,53 @@ export function redactForReader(entry: MemoryEntry, canSeeMasked: boolean): Memo
if (!entry.governance.masked || canSeeMasked) return entry;
return { ...entry, title: '【已脱敏】', ...(entry.detail === undefined ? {} : { detail: {} }) };
}
/**
* 确定性蒸馏骨架 —— **零 LLM**,从结构化日志直接归纳的事实层条目。
*
* 「结构化部分零 LLM,叙述是视图不是事实」(OS 8.1):这一单**确实发生了什么**
* 不需要模型来讲 —— 终态、被纠正的次数、人兜底了几步,都写在归侧条目里。
* 模型可用时在此之上再补模式层与裁决层的归纳;不可用时也不至于什么都留不下。
*
* 每条都带证据链(引用的条目 ID),与 LLM 产出的条目走同一道门。
*/
export function deterministicDistill(entries: readonly MemoryEntry[]): ReadonlyArray<{
layer: 'fact';
statement: string;
entryIds: readonly string[];
}> {
const source = distillable(entries);
if (source.length === 0) return [];
const out: Array<{ layer: 'fact'; statement: string; entryIds: readonly string[] }> = [];
const terminal = source.filter((e) => e.kind === 'terminal');
if (terminal.length > 0) {
out.push({
layer: 'fact',
statement: `本单以「${terminal[terminal.length - 1]!.title}」收场`,
entryIds: terminal.map((e) => e.id),
});
}
// 人兜底的步 = 机器没接住的地方,是资产迭代的首要线索
const fallbacks = source.filter((e) => e.kind === 'card-sent' && e.title.includes('转人兜底'));
if (fallbacks.length > 0) {
out.push({
layer: 'fact',
statement: `有 ${fallbacks.length} 步由机器转人兜底:${fallbacks.map((e) => e.title).join(';')}`,
entryIds: fallbacks.map((e) => e.id),
});
}
const rejections = source.filter((e) => e.kind === 'release-verdict' && !e.title.includes('pass'));
if (rejections.length > 0) {
out.push({
layer: 'fact',
statement: `放行被驳回 ${rejections.length} 次`,
entryIds: rejections.map((e) => e.id),
});
}
return out;
}
@@ -8,7 +8,14 @@ import type { ActivityMemoryEntry, MemoryLayer } from '../domain/activity-memory
import { assertThreePart, MEMORY_LAYERS, selectForLoading } from '../domain/activity-memory.ts';
import { computeMetrics, structuredLog } from '../domain/task-log.ts';
import type { MemoryEntry } from '../domain/task-memory.ts';
import { assertAppendOnly, deriveSide, distillable, redactForReader, windowOf } from '../domain/task-memory.ts';
import {
assertAppendOnly,
deriveSide,
deterministicDistill,
distillable,
redactForReader,
windowOf,
} from '../domain/task-memory.ts';
export type MemoryServiceDeps = {
readonly tasks: TaskMemoryStore;
@@ -114,23 +121,36 @@ export function createMemoryService(deps: MemoryServiceDeps): MemoryService {
/** 写入唯蒸馏:从任务记忆归纳,条目必挂证据链 */
async distill(input: DistillInput): Promise<ActivityMemoryEntry[]> {
const model = await deps.model.systemModelName();
if (model === undefined) return [];
const source = distillable(await deps.tasks.list(input.taskId));
if (source.length === 0) return [];
const { object } = await deps.model.generate({
model,
prompt:
`${input.prompt}\n\n` +
'从以下任务记忆里归纳活动记忆条目。每条必须给出 layer(fact/pattern/verdict/exception)、' +
'statement(一般化陈述)与 entryIds(引用的条目 ID,不得为空)。只输出 JSON。\n\n' +
source.map((e) => `${e.id} [${e.kind}] ${e.title}`).join('\n'),
outputSchema: DISTILL_SCHEMA,
});
/*
* 先落**确定性骨架**(零 LLM):这一单确实发生了什么,写在归侧条目里,
* 不需要模型来讲。模型可用时再在其上补模式层 / 裁决层的归纳;
* 模型不可用(端点不通、未绑定)也不至于什么都留不下 ——
* 「结构化部分零 LLM,叙述是视图不是事实」。
*/
const drafts: Array<{ layer: string; statement: string; entryIds: string[] }> = deterministicDistill(
source,
).map((d) => ({ layer: d.layer, statement: d.statement, entryIds: [...d.entryIds] }));
const drafts = extractDrafts(object);
const model = await deps.model.systemModelName();
if (model !== undefined) {
try {
const { object } = await deps.model.generate({
model,
prompt:
`${input.prompt}\n\n` +
'从以下任务记忆里归纳活动记忆条目。每条必须给出 layer(fact/pattern/verdict/exception)、' +
'statement(一般化陈述)与 entryIds(引用的条目 ID,不得为空)。只输出 JSON。\n\n' +
source.map((e) => `${e.id} [${e.kind}] ${e.title}`).join('\n'),
outputSchema: DISTILL_SCHEMA,
});
drafts.push(...extractDrafts(object));
} catch {
// 模型这一轮归纳不出来:确定性骨架已经落下,不因此丢掉整次蒸馏
}
}
const known = new Set(source.map((e) => e.id));
const now = deps.clock.now().toISOString();