From 61fe18167860af4a521a8a18f5d78ed028d45d18 Mon Sep 17 00:00:00 2001 From: 18628386721 <18628386721@163.com> Date: Mon, 27 Jul 2026 11:44:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=A6=E9=87=8F=E5=85=AB=E6=8C=87=E6=A0=87?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E6=8E=A5=E7=9C=9F:DMN=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=8E=87=E4=B8=8E=E5=8D=95=E4=BD=8D=E6=88=90=E6=9C=AC=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=81=92=E4=B8=BA=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处度量是空的,查下来都是「没人记」而不是「算错了」: ① **DMN 覆盖率恒 0**:判据只认标题里的「求值:」,把跑得最多的**纯工具步**整个漏掉。 DMN 决策表与机检正是落在纯工具/工具步上。改为按步类型分: 确定性求值(pure-tool / tool)÷ 判断总数(再加 LLM 步)——「临场搬进确定」的进度表。 顺带把**步类型显式写进留痕**:判据该由引擎给出,不靠解析标题后缀。 现读数 66%。 ② **单位成本恒 0**:任务消耗只记步数,token / 活跃时长 / 调人次数**从来没人累加** —— 于是八指标里的单位成本永远是 0,预算熔断与打扰上限也形同虚设。 · tick 计这一拍真正花掉的时间(挂起等人的不算活跃,那是世界等待); · flow 的 LLM 步与 loop 的每一回合都把 token 计入; · 每发一张卡计一次调人。 现读数:A2 单轮 12k token、A3 3.4k;单位成本 73 token 每放行单。 另修:执行卡的 ask 优先取 `card.ask`(此前只认 `reason`,新卡面格式下会退化成标题)。 第四轮端到端复跑:整条链闭环,消耗数据真实。 gates: tsc / eslint / depcruise(336 模块)/ api-report / 429 测试 全绿 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m --- .../inbound/e2e-20260728E3@x.com.eml | 12 +++++++ .../inbound/e2e-20260728E4@x.com.eml | 12 +++++++ apps/api/src/app/agent-runner.ts | 3 ++ apps/api/src/app/wiring/desk-elements.ts | 35 ++++++++++++------- apps/api/src/app/wiring/engines.ts | 11 +++++- .../internal/usecase/flow-runner.ts | 3 +- .../task/internal/usecase/task-service.ts | 9 +++-- docs/PROGRESS.md | 32 ++++++++++++++--- 8 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 .data/attachments/inbound/e2e-20260728E3@x.com.eml create mode 100644 .data/attachments/inbound/e2e-20260728E4@x.com.eml diff --git a/.data/attachments/inbound/e2e-20260728E3@x.com.eml b/.data/attachments/inbound/e2e-20260728E3@x.com.eml new file mode 100644 index 0000000..87afea7 --- /dev/null +++ b/.data/attachments/inbound/e2e-20260728E3@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "e2e-20260728E3@x.com", + "from": "buyer@acme.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C860 RFQ-20260728E3 求报价", + "body": "客户编码 C860,询价单号 RFQ-��\nLM358DR 厂牌 TI 年用量 12000 片 目标价 8.20 元 交期 10 周", + "receivedAt": "2026-07-28T12:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/.data/attachments/inbound/e2e-20260728E4@x.com.eml b/.data/attachments/inbound/e2e-20260728E4@x.com.eml new file mode 100644 index 0000000..d884cbe --- /dev/null +++ b/.data/attachments/inbound/e2e-20260728E4@x.com.eml @@ -0,0 +1,12 @@ +{ + "messageId": "e2e-20260728E4@x.com", + "from": "buyer@acme.com", + "to": [ + "rfq@we.com" + ], + "cc": [], + "subject": "【询价】客户 C870 RFQ-20260728E4 求报价", + "body": "客户编码 C870,询价单号 RFQ-��\nTL072CDR 厂牌 TI 年用量 12000 片 目标价 8.20 元 交期 10 周", + "receivedAt": "2026-07-28T12:00:00.000Z", + "attachments": [] +} \ No newline at end of file diff --git a/apps/api/src/app/agent-runner.ts b/apps/api/src/app/agent-runner.ts index c52e0e5..dce6192 100644 --- a/apps/api/src/app/agent-runner.ts +++ b/apps/api/src/app/agent-runner.ts @@ -51,6 +51,8 @@ export type AgentRunnerDeps = { readonly gateway: ModelGateway; /** 活动声明的自定义模型名 */ readonly modelOf: (taskId: TaskId) => Promise; + /** token 回报:计入任务预算 —— 不计,预算熔断就永远不会响 */ + readonly onUsage: (taskId: TaskId, tokens: number) => Promise; }; export function createAgentRunner(deps: AgentRunnerDeps): { @@ -95,6 +97,7 @@ export function createAgentRunner(deps: AgentRunnerDeps): { taskId, label: 'loop-turn', }); + await deps.onUsage(taskId, result.usage.inputTokens + result.usage.outputTokens); return normalize(result.object, enabled); } catch (error) { /* diff --git a/apps/api/src/app/wiring/desk-elements.ts b/apps/api/src/app/wiring/desk-elements.ts index a334243..d5c22f1 100644 --- a/apps/api/src/app/wiring/desk-elements.ts +++ b/apps/api/src/app/wiring/desk-elements.ts @@ -45,18 +45,29 @@ export function wireDeskElements(deps: DeskElementDeps): { minute: MinuteService clock: deps.clock, }); - /** 归侧日志行 → 度量事实。判断类与断点由日志标题的确定形态识别,不另设埋点 */ - const toLogFact = (taskId: string, line: { at: string; side: LogFact['side']; title: string }): LogFact => ({ - taskId, - at: line.at, - side: line.side, - title: line.title, - isHumanToolCall: line.title.startsWith('发出:'), - // DMN / 机检的确定性求值在日志里是「求值:」;LLM 的判断落在「调用:」 - isDeterministicJudgement: line.title.startsWith('求值:'), - isJudgement: line.title.startsWith('求值:') || line.title.startsWith('调用:'), - isBreakpoint: line.title.includes('断点') || line.title.includes('交集为空'), - }); + /** + * 归侧日志行 → 度量事实。零埋点:判据全部取自结构化日志本身。 + * + * **判断次数**包含两类:确定性求值(DMN 决策表 / 机检 —— 落在纯工具与工具步) + * 与临场判断(LLM 步、loop 的语义性前提评估)。 + * DMN 覆盖率 = 前者 ÷ 两者之和 ——「临场搬进确定」的进度表。 + * 早先只认标题里的「求值:」,把跑得最多的纯工具步整个漏掉了,覆盖率恒为 0。 + */ + const toLogFact = (taskId: string, line: { at: string; side: LogFact['side']; title: string }): LogFact => { + const deterministic = + line.title.startsWith('求值:') || line.title.includes('(pure-tool)') || line.title.includes('(tool)'); + const situational = line.title.includes('(llm)'); + return { + taskId, + at: line.at, + side: line.side, + title: line.title, + isHumanToolCall: line.title.startsWith('发出:'), + isDeterministicJudgement: deterministic, + isJudgement: deterministic || situational, + isBreakpoint: line.title.includes('断点') || line.title.includes('交集为空'), + }; + }; const report = createReportService({ store: createReportStore(deps.db), diff --git a/apps/api/src/app/wiring/engines.ts b/apps/api/src/app/wiring/engines.ts index 5284f55..e798e89 100644 --- a/apps/api/src/app/wiring/engines.ts +++ b/apps/api/src/app/wiring/engines.ts @@ -270,6 +270,7 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { criteria: [{ label: '成果齐备', level: 'L5', pass: false }], fields: [], }); + await taskStore.addConsumption(input.taskId, { humanCalls: 1 }); return { id: issued.order.id }; } const fields = Array.isArray(input.card['fields']) @@ -278,9 +279,11 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { const issued = await deps.humanTool.issueExecution(context, { title, basis: basis.length > 0 ? basis : [{ k: '来源', v: '步序声明' }], - ask: String(input.card['reason'] ?? title), + ask: String(input.card['ask'] ?? input.card['reason'] ?? title), fields, }); + // 打扰率的计量对象:每发一张卡就是调了一次人 —— 不计就等于没有上限 + await taskStore.addConsumption(input.taskId, { humanCalls: 1 }); return { id: issued.order.id }; }, }; @@ -348,6 +351,10 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { stepNo: input.stepNo, label: 'flow-step', }); + // token 计入任务预算 —— 单位成本与预算熔断都靠它,不计就等于没有预算 + await taskStore.addConsumption(input.taskId, { + tokens: result.usage.inputTokens + result.usage.outputTokens, + }); // 解析与校验归引擎(它才知道这一步声明了什么);这里只如实上交结果与原文 return { ok: true, @@ -373,6 +380,8 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring { data: taskData, agent: createAgentRunner({ gateway: deps.modelGateway, + // loop 的每一回合都要问模型 —— 不计 token,预算熔断就永远不会响 + onUsage: (taskId, tokens) => taskStore.addConsumption(taskId, { tokens }).then(() => undefined), modelOf: async (taskId) => { const { activityId } = await taskContext.of(taskId); const manifest = await deps.registry.manifestOf(activityId); diff --git a/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts b/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts index 373f526..62eeabd 100644 --- a/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts +++ b/apps/api/src/modules/flow-engine/internal/usecase/flow-runner.ts @@ -128,7 +128,8 @@ export function createFlowRunner(deps: FlowRunnerDeps): FlowEngineService { taskId, kind: 'action', title: `步 ${index + 1} · ${step.label}(${step.kind})`, - detail: { step: step.name, attempt, input: payload }, + // 步类型显式入留痕:度量的判据由引擎给出,不靠解析标题后缀 + detail: { step: step.name, stepKind: step.kind, attempt, input: payload }, }); switch (step.kind) { diff --git a/apps/api/src/modules/task/internal/usecase/task-service.ts b/apps/api/src/modules/task/internal/usecase/task-service.ts index 96a8b86..41bb0aa 100644 --- a/apps/api/src/modules/task/internal/usecase/task-service.ts +++ b/apps/api/src/modules/task/internal/usecase/task-service.ts @@ -133,9 +133,14 @@ export function createTaskService(deps: TaskServiceDeps): TaskService { return { task: finished, verdict, progressed: false }; } + const startedAt = deps.clock.now().getTime(); const outcome = await engineOf(task).advance(taskId); - // 步计量:loop 的一次决策循环 / flow 的一个声明步 - const used = await deps.store.addConsumption(taskId, { steps: 1 }); + // 步计量:loop 的一次决策循环 / flow 的一个声明步。 + // 活跃时长按**这一拍真正花掉的时间**计 —— 挂起等人的时间不算活跃,那是世界等待。 + const used = await deps.store.addConsumption(taskId, { + steps: 1, + activeMs: Math.max(0, deps.clock.now().getTime() - startedAt), + }); await deps.store.markProgress(taskId, deps.clock.now()); const runtime = diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 1781c14..3ff74c3 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -17,7 +17,10 @@ **MVP 十条判据:① ② ③ ⑥ ⑦ ⑧ ⑨ ⑩ 已在真实系统上端到端跑通;④ ⑤ 的 loop 机制(CMMN 装载 / Enabled 集 / 断点 / 调人)跑通,余下部分受限于 LLM 端点。** -**当前:402 个测试通过(含对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。** +**当前:429 个测试通过(含对真实 PG 的集成测试),门禁五道全绿。20 个后端模块 + 5 个包 + 6 个活动包 + 14 个前端 feature。** + +**LLM:DeepSeek provider 已接,两档实例 `deepseek-v4-flash` / `deepseek-v4-pro` + +用途名 `quote-extractor`(→ flash)。密钥只从环境读、落库即密封。** ### MVP 完成判据的跑通进度(README《MVP 完成判据》) @@ -25,15 +28,36 @@ |---|---|---| | 1 | 治理台:员工岗位 / LLM 实例 / 流程包发布 / 触发器 / 邮件通道 | ✅ `pnpm db:seed` 一次做完 | | 2 | RFQ 邮件入站 → 归属解析 → 触发器命中起单 → A1 落到冯兵工作台 | ✅ 已跑通 | -| 3 | A1 受理归一(flow):步序推进 → LLM 抽取 → 台账草稿 → 补全卡 → DMN 打标 → 放行落地 | ✅ 全链路跑通至 `released`:D1 条件跳步、S4 工具、S5 DMN 三分类(LM358DR → Type A)、放行后草稿合并正式版 2 行、输出口 `intake` 发射。**LLM 步因占位 key 走声明给定的人兜底**,接真 key 即走机器路径 | -| 4 | B 类 → A3 渠道寻源(loop) | 🟡 CMMN 装载(21 计划项)、Enabled 集计算、断点判定、Agent 回路调人 —— 全部跑通;**发询 → 数据监听 → 供应商回价续跑需真实 LLM**(Agent 每一轮决断都要模型) | -| 5 | C 类 → A2 替代选型(loop) | 🟡 同上:CMMN 装载(19 计划项)+ 回路推进跑通;跨人卡与交付放行需真实 LLM | +| 3 | A1 受理归一(flow):步序推进 → LLM 抽取 → 台账草稿 → 补全卡 → DMN 打标 → 放行落地 | ✅ **机器路径全程跑通,零人工兜底**:模型从询价原文抽出 pn/brand/eau/目标价/交期 → D1 条件跳步 → S4 工具 → S5 DMN 三分类 → 放行 → 草稿合并正式版 → 输出口发射 → `released` | +| 4 | B 类 → A3 渠道寻源(loop) | 🟡 CMMN 装载(21 计划项)、Enabled 集、断点判定、技能装配入信封、Agent 真实决策(接 DeepSeek 后每回合由模型定夺)、案例全景视图 —— 全部跑通;**多轮寻源直至交付放行**受限于回合成本,未跑满 | +| 5 | C 类 → A2 替代选型(loop) | 🟡 同上:CMMN 装载(19 计划项)+ 回路推进 + 技能装配 + 案例全景视图跑通;跨人卡与交付放行未跑满 | | 6 | A4 成本定价(flow) | ✅ 取价基准 → DMN 定价(利润模型 P-01)→ 算偏离率 → DMN 底价自检(底价门禁 P-02)→ 条件跳步 → 报价草案验收放行 → released | | 7 | AP 定价审批(审批点) | ✅ 卡到 PM 丁子乔工位,依据快照齐;**驳回必附意见**(空意见被拒)、**驳回 → 回 A4 重开新实例**(A4 #2,旧实例不复活)、**通过 → 进 A6** | | 8 | A6 报价议价(flow) | ✅ 正式报价单生成 → 写类动作**登记为动作意向**(不碰世界)→ 放行即落地(台账回写 1 行)→ 落地收敛 → 输出口发射 | | 9 | 客户拒绝 → A7 情报归档 | ✅ 出口选 lost → G4 按 `outcome = "lost"` 判定 → A7 起单 → 历史价格情报回写(拒绝原因)、客户画像、落地率统计落账 → **end 节点 released,工作流「已完成」** | | 10 | 全程可审计 · 工作台五要素 · 治理台在途监控与叫停恢复 | ✅ 任务记忆只追加、信封盖戳、归侧标注(实测 A7:机器产出 13 / 人裁决 3 / 世界等待 2 / 人产出 1);五要素齐活,纪要「决议 → 登记起单」端到端跑通;治理台在途监控 + 叫停 + 恢复跑通且留痕归人裁决侧;**终态封卷蒸馏钩子产出活动记忆候选条目**(确定性骨架,零 LLM,条目带证据链) | +## 活动资产盘点(六个活动) + +| 活动 | 范式 | 视图模版 | 技能 | 结晶片段 | +|---|---|---|---|---| +| A1 RFQ 需求分析 | flow | 事态全景 / 打标判据 / 成果预览 | 读懂一封询价 · 打标前先备齐判据 | 1 | +| A4 成本定价 | flow | 取价基准 / 定价与门禁 / 草案待放行 | 选对价格基准 · 读懂底价门禁的处置 | 1 | +| A6 报价议价 | flow | 正式报价单 / 放行即发出 | 拟一份能发出去的报价单 · 选对议价出口 | 0 | +| A7 情报归档 | flow | 结果登记 / 闭环待放行 | 把拒绝原因写成下一单的判据 | 0 | +| A3 渠道寻源 | loop | 案例全景(阶段 / 可为 / 缺口) | 按场景圈渠道 · 该等还是该问 | 0 | +| A2 替代选型 | loop | 案例全景 | 圈替代候选 · 什么时候必须问客户 | 0 | + +视图外壳与卡片件在 `@sdk`(`viewShell` / `kvCard` / `tableCard` / `stagesCard` / `noteCard`)—— +六个活动共用一套面貌;容器是 `sandbox=""` 的 iframe,样式内联自带。 + +## 端到端跑过的轮次 + +四轮完整链路(入站 → A1 → G2 → A3/A2 → 汇合 → A4 → AP → A6 → G4 → A7/end): +拒绝分支(lost → A7 → 终点)三轮、成交分支(won → 终点)一轮,均收敛为「已完成」。 +度量八指标全部有真实读数(自主率 100% / 有效自主率 95% / DMN 覆盖率 66% / +单位成本 73 token 每放行单 / 终态分布 released 95%)。 + ## 常用命令 ```bash