retaintive LLM 平台架构终态与 Traceability

Date: 2026-06-24 Status: Draft(mental-model 对齐用,不含代码改动) Scope: 平面架构(Control / Data / Cross-cutting)/ execution bundle / ID 传播 / traceability 三层 / 客户可见性 / agent 演进路径


0. 这份文档解决什么

我们已经有一份 LLM Application Foundation Concepts,它讲清了「一条 LLM 请求的链路」和「每一层负责什么」。但它有一个结构缺口:

它把链路画成一条竖直流水线(trigger → context → builder → inference → writer → ledger → eval)。 这容易让人误以为:observability / eval 是「请求跑完之后」才发生的最后一步,control 是「中间一道工序」。

真实系统不是一条直线。它是 三个平面叠在一起

  • Governance / Release Plane(治理/发布面) —— 不处理单条请求,管「这次请求被允许用哪个 prompt / model / tool / policy 版本」。
  • Data Plane(数据面) —— 一条请求真正怎么走:取 context → 组装 → 调模型 → 裁决 → 写库。
  • Cross-cutting Plane(横切面) —— 从入口到出口一直存在的 trace / audit / authz / idempotency。

这份文档的核心产出是一张 plane 架构图(§2),把 retaintive 每个真实组件(contacts-analyzer、Traceplane、Policy Guard、Neon、common taxonomy)钉到正确的平面上,然后回答四个你一直在问的问题:

  1. execution bundle 是什么、在哪一层 resolve(§3)
  2. IDrequestId / aiRunId / contactAnalysisRunId / promptHash)怎么穿过全链路(§4)
  3. traceability 三层(Neon / Decision Ledger / Traceplane)各负责什么、给客户展示什么(§5)—— 这是最关键的一节
  4. 未来加 tool calling / agent execution 时,怎么不破坏前面三点(§6)

⚠️ 重要 frame:这份文档讲的不是「封闭环境银行自建 GPU/vLLM」。retaintive 是多租户 SaaS、走 OpenRouter、serving 层不自建。所以 GPU / KV cache / continuous batching / vLLM 调度全部不在我们的范围——它们是理解概念用的,不是我们要做的事。我们的工程重点全在 Governance Plane、Data Plane 的 retaintive-specific 部分,和 traceability。


1. 先纠正一个认知:retaintive 现在比你以为的成熟

在画终态之前,先对齐现状(2026-06-24,读 live code 确认,附 file:line)。很多人(包括外部 AI 讨论)会假设「你得从零搭 traceability」——这是错的

组件状态证据
promptVersion + promptHash(SHA256 over ~45KB assembled prompt)✅ 已有,写进 task_suggestions / task_playbooks / tasksprompt-version.ts:15ai-client.ts:103prompt-builder.ts
contactAnalysisRunId(= SQS messageId,一次 Lambda run 的 ID)✅ 已有,写进 tasks.contact_analysis_run_idneon-repository.ts:748
aiRunStartedAt(human-authority guard:staff 在 AI run 之后改过的 task,AI 不能覆盖)✅ 已有,是 deterministic writer guardprotocols.ts:160handler.ts:202
Traceplane(一次 AI run 的完整 debug 快照 ingest)✅ 已有,但 default disabled、是内部 observability sink、只记 input + proposal,不记 accept/reject 结果traceplane-publisher.tslib/config/environments.ts:25
Policy Guard(DNC / store / duplicate / type allowlist / stale proposal)✅ 已有@retaintive/common/domain applyTaskAction()
Task Accuracy Foundation(57 golden eval + 11 DB replay)✅ 已有ai-agent-foundation-final-state.md §0
Tool calling❌ 完全没有,纯 "push context" 模式(代码查好 Neon 数据塞给模型,模型只 generateObject() 结构化输出)invoke.ts,无 tools 参数
Decision Ledger(rejected / skipped / zero-row proposal 的裁决账本)缺口 —— 被拒的 proposal 现在静默消失,违反世界观死规矩「绝不静默丢弃」

所以你的 plan 不是「造轮子」,而是「收敛」:把已经散落的 Traceplane + promptHash + aiRunStartedAt + 那份 concepts draft 的 Layer 1-4,收敛成一个连贯的、和官方六层基座对齐的三平面模型,并补上唯一真正缺的 Decision Ledger。


2. 核心图:三平面架构(retaintive 落地版)

这张图替代 concepts draft 里那条竖直流水线。每个方框都是 retaintive 真实组件,不是泛泛模板。

╔══════════════════════════════ GOVERNANCE / RELEASE PLANE(治理/发布面)═══════════════════════════════╗
║  不处理单条请求。管「这次请求被允许用哪个版本 / 能力」。改这里要走 review + eval + 发布。  ║
║                                                                                      ║
║   Prompt Registry            Model / Provider Routing      Taxonomy / Metadata        ║
║   prompt-version.ts          provider-routing.ts           @retaintive/common         ║
║   (promptVersion+Hash)       (OpenRouter routing)          /taxonomy (contact+task)   ║
║                                                                                      ║
║   Output Schema              Tool Catalog(未来)           Policy / Threshold Config   ║
║   ContactsAnalysisSchema     (现在为空)                   DNC / store / 阈值 env       ║
║   (Zod, models.ts)                                         (LEAD_*_THRESHOLD)         ║
║                                                                                      ║
║   Tenant Config(方向已定,schema 预埋 tenant_id,当前 nullable)                       ║
║                                                                                      ║
║                          ┌─────────────────────────────────┐                         ║
║                          │  resolve immutable               │                         ║
║                          │  EXECUTION BUNDLE(见 §3)        │ ← 把上面所有版本钉成一个  ║
║                          │  promptVersion + promptHash +    │   不可变组合,本次 run    ║
║                          │  model + schema + taxonomy +     │   全程只用这一个 bundle   ║
║                          │  policy + tenantConfig versions  │                         ║
║                          └────────────────┬────────────────┘                         ║
╚═══════════════════════════════════════════│══════════════════════════════════════════╝
                                            │ 注入本次 run(read-only)

╔══════════════════════════════ DATA PLANE(数据面)═════════════════════════════════════╗
║  一条请求真正怎么走。retaintive 当前是「push context」——代码查好数据塞给模型。            ║
║                                                                                      ║
║  ┌── 入口 / Trigger ───────────────────────────────────────────────────────────────┐ ║
║  │  daily cron  |  per-call SQS FIFO  |  message_ingest  |  Refresh AI / reprocess   │ ║
║  │  handler.ts: 生成 contactAnalysisRunId(=messageId), aiRunStartedAt, storeId,       │ ║
║  │              tenantId, source                                                     │ ║
║  └────────────────────────────────────┬────────────────────────────────────────────┘ ║
║                                       ▼                                                ║
║  ┌── Context 获取(permissioned)──────────────────────────────────────────────────┐  ║
║  │  Neon reader: contact snapshot + calls + messages + leads + open/closed tasks     │  ║
║  │  全部带 storeId / tenantId scope(隔离在 SQL,不在 prompt)                          │  ║
║  │  渲染 sourceRef C1/M1/L1、taskRef T1/T2(task-refs.ts)                             │  ║
║  └────────────────────────────────────┬────────────────────────────────────────────┘  ║
║                                       ▼                                                ║
║  ┌── LLM Request Builder ─────────────────────────────────────────────────────────┐  ║
║  │  buildSystemPromptResult(): 稳定 sections(role/purpose/evidence/policy/taxonomy)│  ║
║  │  buildUserMessage(): 本次 contact 的动态事实                                       │  ║
║  │  attach: ContactsAnalysisSchema(走 request body,不手写进 prompt)                 │  ║
║  │  ← prompt 分层工作主要改这一层                                                        │  ║
║  └────────────────────────────────────┬────────────────────────────────────────────┘  ║
║                                       ▼                                                ║
║  ┌── Inference(外部,OpenRouter)────────────────────────────────────────────────┐   ║
║  │  invoke.ts: generateObject({ schema, system, prompt })                           │   ║
║  │  ⚠️ prefill/decode/KV cache/batching 都在 OpenRouter 那边,不是我们的事              │   ║
║  └────────────────────────────────────┬────────────────────────────────────────────┘   ║
║                                       ▼                                                ║
║  ┌── 模型输出 = PROPOSAL(不是事实)──────────────────────────────────────────────┐   ║
║  │  contact field updates + taskDecisions[](create_open/close/update/...)          │   ║
║  └────────────────────────────────────┬────────────────────────────────────────────┘   ║
║                                       ▼                                                ║
║  ┌── Runtime Control Gate(真正的权威,§见世界观)──────────────────────────────────────┐  ║
║  │  schema parse → resolve T1/C1/M1 refs → map to common TaskAction                  │  ║
║  │  → Policy Guard: DNC / store mismatch / duplicate / hallucinated taskId /         │  ║
║  │                  stale(aiRunStartedAt guard)                                       │  ║
║  │  → 裁决: accepted | rejected | skipped | zero-row                                 │  ║
║  │  → DB transaction(db.batch 原子写)                                               │  ║
║  └──────────────┬──────────────────────────────────────┬──────────────────────────┘  ║
║                 │ accepted 写业务状态                    │ 所有裁决(含 rejected)        ║
╚═════════════════│══════════════════════════════════════│═══════════════════════════════╝
                  ▼                                       ▼
        ┌─────────────────────┐              ┌──────────────────────────────┐
        │ Neon system-of-record│              │ Decision Ledger(§5 缺口)    │
        │ tasks / task_sources │              │ accepted/rejected/skipped     │
        │ contact_timeline     │              │ + reason + evidence + bundle  │
        │ → 客户在 Studio 看到   │              │ → 内部 + 未来客户"为什么这么判" │
        └─────────────────────┘              └──────────────────────────────┘

╔══════════════════════════ CROSS-CUTTING PLANE(横切面,全程存在)═══════════════════════╗
║  不是「最后一步」。从入口到出口每一层都在产出。                                            ║
║                                                                                       ║
║  Trace / Run Observability        Audit / Authz                  Idempotency           ║
║  Traceplane(内部 debug 快照)     storeId/tenantId scope         aiRunStartedAt guard   ║
║  ai_usage CloudWatch log          Policy Guard                   SHA256 trackingId      ║
║  promptVersion/Hash               (死红线在代码,不在 prompt)    (防 retry 重复写)       ║
║                                                                                       ║
║  Evaluation(异步,不阻塞请求)                                                          ║
║  57 golden eval + 11 DB replay + Traceplane replay → 改 prompt/schema/writer 前后跑     ║
╚═══════════════════════════════════════════════════════════════════════════════════════╝

2.1 怎么读这张图(三句话)

  1. Governance Plane 在上面,它不碰单条请求,只「发版本」。它 resolve 出一个 execution bundle 注入到 Data Plane。改 prompt/model/schema = 改 Governance Plane,必须走 eval + 发布。
  2. Data Plane 在中间,是请求真正的路径。retaintive 现在是「push context」单向流,未来加 tool calling 会让它变成循环(§6),但 Runtime Control Gate 那一关永远不变。
  3. Cross-cutting Plane 包住全部。Traceplane、Policy Guard、idempotency、eval 不是某一道工序,是贯穿全程的横切关注点。

⚠️ 命名澄清(避免和官方六层文档冲突):官方 ai-agent-foundation-final-statePolicy Guard / approval 那一层叫 "Control Plane"。本文档为了不混淆,把两个不同的东西用不同名字:

  • Governance / Release Plane = 管「版本和能力发布」(prompt/model/schema/taxonomy/policy 版本)—— 本图最上面那层,不是官方说的 Control Plane。
  • Runtime Control Gate = 运行时每条请求都要过的 Policy Guard / 裁决关 —— 它才对应官方六层的"第 4 层 Control Plane",在 Data Plane 内部。

一句话:「发版本的治理」和「拦请求的裁决」是两件事。前者慢、影响所有请求、走发布流程;后者快、每条请求都过、代码权威。

2.2 这张图怎么对上官方六层基座

本图平面对应 ai-agent-foundation-final-state 六层
Governance / Release Plane第 3 层 AI Decision Layer(LLM Gateway / prompt / model routing)的"治理面"
Data Plane: Context 获取第 1-2 层 Facts + Objective/Context Builder
Data Plane: Builder + Inference第 3 层 AI Decision Layer 的"执行面"
Data Plane: Runtime Control Gate第 4 层 Control Plane(Policy Guard / approval)
Neon system-of-record第 1 层 Facts + 第 6 层 Feedback 的可见面
Cross-cutting: Traceplane / Ledger / Eval第 4 层 audit + 第 6 层 feedback 的 observability

所以这张图不是另起炉灶——它是把官方六层"侧过来看":六层是数据上下游,三平面是「谁管版本 / 谁跑请求 / 谁全程盯着」。


3. Execution Bundle:把「版本」钉成一个不可变组合

3.1 问题:为什么不能只记 promptVersion

一次 LLM 行为不是只由 prompt 决定。同一段 prompt,换了 model、换了 taxonomy、换了 Policy 阈值,输出可能完全不同。如果只记 promptVersion = v17,出问题时你无法复现当时模型到底在什么组合下做的判断。

retaintive 现在已经记了一部分(promptVersion + promptHash + model),但它们散在不同地方,没有钉成一个单一不可变 ID

3.2 Execution Bundle = Governance Plane resolve 出的不可变组合

execution_bundle_id: contacts-analyzer-prod-2026-06-24.<hash>

  prompt:
    version: contacts-analyzer-v17-2026-06-23   # 已有
    hash: sha256:...                            # 已有(45KB assembled)
  model:
    name / provider                             # 已有(ai_usage log)
    generation params (temperature/maxTokens)   # 已有
  schema:
    ContactsAnalysisSchema version              # 部分(schema 名有,version 待补)
  taxonomy:
    contacts-ai-taxonomy-v3-2026-06-19          # 已有(taxonomyVersion)
  policy:
    DNC / store / threshold (LEAD_*_THRESHOLD)  # 已有(env,但没纳入 bundle id)
  tenant:
    tenantConfig version                        # 方向已定,未启用

关键约束(§见世界观「身份先理顺」的同源逻辑):一次 aiRunId 从头到尾只用一个 bundle。不能第一次模型调用用 v17,工具回来第二次调用突然变 v18。这在未来 tool-calling 循环(§6)里尤其重要。

3.3 这一步要做的(最小)

不需要马上建一张 execution_bundles 表。第一步只是:

  1. 把现在散落的 promptVersion / promptHash / model / taxonomyVersion / schemaName 在 handler 入口算成一个 executionBundleId(SHA256 over 这些值)
  2. 同时把可读的 components 一起记下来,不能只记 hash(见下方关键点)。
  3. 把这个 id + components 跟着 contactAnalysisRunId 一起,写进 Neon 写入路径 + Traceplane + Decision Ledger。
  4. 这样任何一条 task / 任何一个 rejected proposal,都能回答「它是哪个 bundle 产生的」。

关键:hash 只能"追责",不能"解释"。 如果只存 executionBundleId = sha256:abc,半年后 env threshold 改了、OpenRouter model alias 指向了新模型、taxonomy package 升级了,光看 hash 无法还原当时到底用了什么。所以 bundle 必须同时存可读 components(resolved model 名 + 实际 provider + temperature/maxTokens + schemaVersion + 各 version 字符串 + threshold 实际值)。hash 用来快速比对「这两次 run 是不是同一组合」,components 用来回答「这次 run 到底用了啥」。

另一个 OpenRouter 现实约束:bundle 记的是 resolved provider/model(OpenRouter 实际路由到的那个),不是请求时写的 alias。而且即使 components 全记下来,也只能近似复现 + 追责,不能保证 deterministic reproduction——LLM 输出本身有随机性。bundle 的目的是 attribution(归因),不是 bit-level replay。

这是「收敛」不是「造轮子」——所有原料都已经存在,只是没钉在一起。升级到独立 execution_bundles 表的 trigger:当 bundle components 开始被多处 join 查询、或 tool calling 让一次 run 产生多个不同 bundle 时,再抽表。现在内联记录足够。


4. ID 传播:哪些 ID 穿过全链路,哪些不传

你一直问「requestId / aiRunId 这些 ID 是不是每层都传」。答案是:分三类,不是全部都一路传到底

4.1 三类 ID

A. 追踪 / 关联类(贯穿全链路)
   trace_id            整条分布式调用链(未来接 OpenTelemetry 时)
   aiRunId             一次完整 AI 工作流(retaintive 现在 = contactAnalysisRunId = messageId)
   executionBundleId   本次 run 用的版本组合(§3)

B. 业务 / 安全类(按需,不随便全传)
   tenantId / storeId  隔离边界 —— 用于 SQL scope / Policy Guard,不需要传给模型 prompt
   contactPhone        业务 key —— 渲染进 context,但敏感,不进 trace baggage
   conversationId      多轮对话(未来 chat/voice agent 才需要)

C. 执行 / 幂等类(写入侧)
   idempotency / trackingId   SHA256(franchiseId:storeId:phone+target),防 retry 重复写(Traceplane 已有)
   aiRunStartedAt             human-authority guard(已有)
   taskRef T1 / sourceRef C1  prompt 里的占位,真实 UUID 解析在 code,不在 prompt

4.2 retaintive 现状 vs 终态

现状(已经对的):
  contactAnalysisRunId ──┬── tasks.contact_analysis_run_id
                         ├── Traceplane run.contactAnalysisRunId
                         └── ai_usage CloudWatch log

  promptVersion/Hash ────┬── task_suggestions / task_playbooks / tasks
                         └── Traceplane promptMetadata

  aiRunStartedAt ────────── Policy Guard(staff-edit-wins)

终态补的(收敛):
  executionBundleId ─────┬── 同上所有写入点都带上
                         └── Decision Ledger 每条裁决都带上
  aiRunId ───────────────── 显式区分于 contactAnalysisRunId(一次 run 可能产生多个 task,
                            未来 tool loop 里一次 run 有多次 llm call,需要 1:N 关系)

一个常见误区conversationId(应用层多轮会话)≠ serving 层的 sequence id。retaintive 现在连 conversationId 都不需要(不是多轮 chat)。等做 voice agent 才需要。别提前引入。


5. Traceability 三层:Neon vs Decision Ledger vs Traceplane(最关键)

这是你那个问题的核心:「Traceplane 和 Neon 分别负责什么?该用什么方式把什么东西展示给客户?」

答案是:它们是三个不同的东西,受众、一致性、生命周期完全不同。最常见的错误就是把它们混成一坨。

5.1 三层对照表

它是什么受众一致性客户可见?retaintive 现状
① Neon system-of-record经过把关后被接受的业务状态 + 业务级 audit客户(Studio UI)+ studio-api强一致、事务、可 join(核心产品面)tasks / task_sources / task_progress_events / contact_timeline
② Decision LedgerAI proposal 的裁决账本:accepted/rejected/skipped/zero-row + reason + evidence内部为主;未来投射成客户「AI 为什么这么判」强一致(死规矩「绝不静默丢弃」)⚠️ 部分(投射后)缺口
③ Traceplane一次 AI run 的完整 debug 快照(模型看到啥、prompt 哪个版本、token/cost)纯内部(debug / eval / replay / 模型漂移归因)最终一致、fail-open、可丢永远不直接给客户✅ 有,但只记 input+proposal、default disabled

5.2 Traceplane 到底在做什么(读 live code 的定性)

traceplane-publisher.ts + lib/config/environments.ts 确认:

  • 它是 "Optional fail-open ingest sink for AI analysis runtime events"environments.ts:25 原注释)。
  • handler 调用后失败只 logger.warn不阻断主流程handler.ts:532)—— fail-open。
  • payload 把真实 taskId redact 成 T1/C1/M1 ref(redactTaskDecisionIds)—— 是给分析/replay看的。
  • 词汇是 recipeLabel / target: task_transition_generate / mode: append_and_run_async —— 这是 eval/replay recipe 语言,不是产品 UI 语言。

定性结论:Traceplane 是内部 AI-run observability / eval ingest,是给团队「回放某次 run 为什么这么判」用的。它不是给客户的产品溯源层,也不该变成。

5.3 那「给客户展示什么」?—— 三个不同的可见性层级

这是 plan 的产品论点:

给客户看的(产品面,走 Neon):
  ┌─────────────────────────────────────────────────────────┐
  │ Studio: 这个跟进任务为什么存在?                            │
  │   task → task_sources → C1/M1/L1 → 真实 call/message       │
  │   "因为客户在 6/22 这通电话里说想升级会籍"                   │
  │   ← 客户要的是 evidence 链,不是 prompt hash               │
  └─────────────────────────────────────────────────────────┘

给客户看的进阶(差异化,走 Decision Ledger 投射):
  ┌─────────────────────────────────────────────────────────┐
  │ Studio: 为什么"没有"建某个任务?                            │
  │   "我们判断客户想升级,但因为标了 DNC,没有创建外呼任务"      │
  │   ← 这是世界观"绝不静默丢弃"的产品化,竞品做不到 = 差异化     │
  └─────────────────────────────────────────────────────────┘

永远不给客户看的(内部,走 Traceplane / ai_usage):
  ┌─────────────────────────────────────────────────────────┐
  │ promptHash / model / token / cost / prefill 细节           │
  │ ← 内部 debug,混进产品 = 泄漏实现 + 噪音                     │
  └─────────────────────────────────────────────────────────┘

5.4 所以推荐(不要把 Traceplane 改成给客户看)

① Neon       做厚 task_sources evidence 链 → 客户可见"为什么有这个任务"(已有,继续)
② Ledger     补 Decision Ledger 进 Neon(强一致)→ 内部 + 可投射成客户"为什么没这个任务"(补缺口)
③ Traceplane 保持内部 run observability,做厚成"也记裁决结果"→ 永远不直接面客(演进现有)

为什么 Decision Ledger 要进 Neon 而不是塞进 Traceplane:因为「绝不静默丢弃」是世界观死规矩,它要求强一致、可查、可 join、可 DB replay。Traceplane 是 fail-open 可丢的——把死规矩放在一个「可丢」的 sink 上是矛盾的。Traceplane 记的是「这次 run 的全貌(debug 用)」,Ledger 记的是「每条 proposal 的最终命运(合规/产品用)」。两者都要,但不能合并。

5.5 Decision Ledger 的四条硬 contract(设计 ledger 前必须先定,否则会埋坑)

「补一个 ledger」听起来简单,但如果不先定清楚下面四条,它会变成一张又大又没人信的表。这四条是写 schema 前的前置决策。

① Transaction 语义:ledger 写入必须和业务 mutation 同一个原子事务

这是最关键的一条。如果业务 mutation 成功了但 ledger 写失败(warn 后继续),就正好制造了「静默丢弃」——明明改了库却没留下裁决记录。所以:

✅ 正确:accepted/rejected/skipped 的 ledger 行,和 Writer 的 DB 改动在同一个 db.batch() 原子事务里
        ledger 写失败 = 整个 run 失败 / retry,不能 warn-and-continue
❌ 错误:先写业务表,再 fire-and-forget 写 ledger(= Traceplane 的 fail-open 模式,不适用于死规矩)

这正好复用 repo 已有的 atomic-batch-design 硬约束db.batch() 原子、pre-batch guard)——ledger 不是新模式,是把现有原子写扩一行。

② 粒度 contract:什么进 ledger、什么不进

进 Ledger(business-material 裁决事实):
  create_open / create_closed / close / update / record_progress 的 accepted
  被 Policy Guard 拒的 rejected(DNC / store mismatch / duplicate / hallucinated taskId / stale)
  应该建但没建的 skipped(如证据不足)
  schema parse failure(模型输出不合 schema)
  zero-row(proposal 解析后没匹配到任何真实 task)
  human override(staff 改了 AI 的判断)

不进 Ledger(归 Traceplane debug):
  模型中间 reasoning / 草稿
  read-only retrieval attempt(未来 tool 只读查询)
  每个 prompt section 的 hash 细节
  token / cost / latency

一句话:Ledger 记「proposal 的最终命运」,Traceplane 记「这次 run 怎么得出的」。

③ Tenant / PII 边界:ledger 和它的客户投射,必须和 tasks 一样严格 scoped

Decision Ledger 行必须带 tenantId / storeId,查询强制 scope(和 tasks 同等级隔离)
客户投射(§5.3 的"为什么没建任务")走 studio-api,必须经过 authz + PII redaction
Traceplane 快照含 raw context(可能有 PII)→ 内部访问控制 + retention,永不直连客户

④ Customer-visible reason taxonomy:给客户看的拒绝理由要产品化

不要把内部 Policy Guard 的 debug 文案直接暴露给客户。要有一层 enum 映射:

内部 reason            →  客户可见 reason(产品化)
dnc_internal_flag      →  dnc_blocked("该客户已设置请勿联系")
duplicate_open_taskid  →  duplicate_open_task("已有相同的待跟进任务")
identity_conflict      →  needs_review_identity_conflict("客户身份待确认")
stale_proposal         →  (通常不展示给客户,内部 only)

这层映射本身也属于 Governance Plane(taxonomy),从 common 渲染,不在 prompt 手写。

不现在做的(避免过度设计):retention / partitioning / 归档策略。retaintive 现在是 daily-batch 量级,ledger 不会膨胀。但 tool calling 上线后量级会涨——那时再设计 retention,作为 §6 Step 3 的前置条件。现在留这句话当 trigger 就够,不提前建 partitioning。


6. 未来演进:加 tool calling / agent execution 时怎么不破坏前面

retaintive 现在是 push context(代码查好塞给模型,单向)。世界观 §四 和官方六层都指向未来要 agent 自己取 context / 执行动作。但演进顺序很重要——别跳步。

6.1 push → pull 的演进,Data Plane 从单向变循环

现在(push,单向):
  Context 获取 → Builder → 模型 → proposal → Writer → DB

未来(pull + tool loop):
  Builder → 模型 → tool call proposal

              Policy Guard 校验能力 + 权限(capability envelope)

              Execution Service 执行(受控,不是 AI 直接调 API)

              结果回 Context → 再次 Builder(用同一个 execution bundle!)

              模型 → 最终 proposal → Writer → DB

              每一步都进 Decision Ledger + Traceplane

不变的内核(世界观死规矩):模型只能 propose,确定性代码 authorize,Execution Service execute,每个动作(成功/被拒/尝试)记账。tool loop 让链路变复杂,但这条不变。

6.2 演进的正确顺序(别跳步)

Step 1(现在)  把 push context 做对、可追溯、可评估
                → execution bundle 收敛 + Decision Ledger 补齐 + Traceplane 记结果
                → 先证明"模型为什么建/没建 task"完全可查

Step 2          只读 self-check agent(不执行动作,只复核)
                → 用现有 Traceplane replay + eval 喂给一个 verify agent
                → ⚠️ 这一步【不被 Decision Ledger 阻塞】——它不动 DB、不触达客户,
                   靠 Traceplane replay 就能跑。可以和 Step 1 并行起步

Step 3          tool calling(先只读 tool:查 CRM/POS 状态)
                → 前置 gate(缺一不可):Tool Catalog + capability envelope +
                   tenant-scoped tool authz + prompt-injection 防护 + rate/cost limit
                → ⚠️ 即使是【只读】tool,没有 tenant scope 也可能跨租户取数 / 泄漏 context

Step 4          outbound execution(Vapi 打电话 / 发 SMS)
                → approval queue + agent execution log + LLM Gateway
                → 这些正是 ai-agent-foundation-final-state 列的"Demo 后"缺口
                → ⚠️ Decision Ledger 是【这一步】(customer-impacting / outbound)的硬前提

为什么这个顺序

  • Decision Ledger 是 Step 4(outbound / customer-impacting)的硬前提,不是 Step 2 的。如果 AI 要真的打电话/发消息,每个动作的命运必须强一致留痕——否则「静默丢弃」会变成「静默对客户做了事却查不到」。
  • 但 Step 2(只读 self-check)不必等 Ledger——它只读、不触达客户,靠 Traceplane replay 就能跑,可以早做。
  • Step 3 的 tool 即使只读也要先有 tenant-scoped authz——「只读」不等于「安全」,跨租户读取同样是隔离事故。

一句话修正:先把 traceability 做厚,是 outbound agent 化的前提;read-only 的复核/工具可以更早起步,但必须先有 tenant 隔离。


7. 这张图最重要的用途:新功能「该加在哪」决策表

这才是分平面的首要目的——不是为了现在把所有东西建完,而是建立一张放置规则表:以后任何新功能一来,立刻知道它属于哪一层、跟哪些 ID 挂钩、要不要进 Ledger / Traceplane。框架先行,功能后填。

7.1 决策流程(拿到一个新功能,先问三个问题)

新功能来了

   ├─ Q1: 它是「管版本/能力/配置」还是「跑一条请求」?
   │     管版本/能力/配置(改了影响所有请求,要走 eval+发布) → Governance Plane
   │     跑一条请求(处理单条 contact/call 的数据流)         → Data Plane
   │     全程都要(trace/audit/幂等/eval)                    → Cross-cutting Plane

   ├─ Q2: 它会让 AI「动手做事」还是只「提建议」?
   │     只提建议(proposal)                → 输出仍走 Writer 裁决,不碰 capability envelope
   │     会动手(发消息/打电话/改外部状态)   → 必须进 capability envelope + approval + execution log

   └─ Q3: 它产生的东西,谁要看?
         客户要看「为什么有/没有这个任务」    → 投射 Neon evidence 链 / Decision Ledger
         内部要 debug「这次 run 为什么这样」   → Traceplane
         合规要「这条 proposal 最终命运」      → Decision Ledger(强一致,不能用 Traceplane)

7.2 放置规则表(常见新功能 → 加在哪)

如果你要加…加在哪一层挂哪些 ID / 进哪个 ledger为什么
新的 prompt 写法 / 新 sectionGovernance Plane: Prompt Registrybump promptVersion + 重算 promptHash → 进 execution bundle改了影响所有请求,必须可复现可回滚
换 model / 加 fallback modelGovernance Plane: Model Routing进 execution bundle 的 model 字段同一 prompt 换 model 输出会变,必须钉进 bundle
新的 task 类型 / 新 enumGovernance Plane: Taxonomy(@retaintive/commonbump taxonomyVersion → 进 bundle从 common 渲染,绝不在 prompt 手写第二份
新的 Policy Guard 规则(如新 DNC 条件)Data Plane: Runtime Control Gate裁决结果进 Decision Ledger死红线在代码,不在 prompt;拒了要留痕
新的 context 源(如接 CRM 只读)Data Plane: Context 获取带 storeId/tenantId scope;source 进 Traceplane 快照隔离在数据访问层,不在 prompt
新的 AI 输出面(如 coaching / call 分类的新字段)Data Plane: 新 schema + Builder复用同一 execution bundle + Decision Ledger「共享底座 + 薄 harness」,不另起一套
新的阈值(如 lead outreach 天数)Governance Plane: Policy/Threshold Config进 bundle 的 policy 字段部署级配置,影响裁决,要可复现
新渠道入口(如接 Salesforce 事件)Data Plane: 入口/Trigger生成 aiRunId / 带 source「进门平等、信任不平等」——底层不为渠道写特例
让 AI 自己查数据(pull context / tool,即使只读Governance Plane: Tool Catalog + Data Plane: tool looptool_call_id + capability envelope + tenant-scoped tool authz + Ledger§6 Step 3;只读也要 tenant 隔离,否则跨租户取数
让 AI 自己发消息/打电话Governance Plane: capability envelope + Data Plane: Execution Serviceapproval queue + agent execution log + Ledger§6 Step 4,世界观死规矩:受控代码执行、全程记账
给客户展示「AI 为什么这么判」投射层(读 Neon evidence 链 + Decision Ledger)不读 Traceplane客户看 evidence + 裁决,不看 prompt hash/token
内部要 debug 某次 runCross-cutting: Traceplane replay用 aiRunId / executionBundleId 查Traceplane 是内部 observability,不面客
模型/prompt 漂移监控、改 prompt 前后对比Cross-cutting: Evaluationgolden eval + DB replay + Traceplane replayeval 是发布流程一部分,不阻塞请求

7.3 三条「放错地方」的红线(最容易犯的错)

❌ 把死红线写进 prompt
   DNC / store 隔离 / 写入权限 → 必须在 Data Plane / Writer 的代码里
   (prompt 只能帮模型少犯错,不能当权威。世界观死规矩)

❌ 把「绝不静默丢弃」放进 Traceplane
   rejected/skipped proposal → 必须进 Neon Decision Ledger(强一致)
   (Traceplane 是 fail-open 可丢的,放死规矩在可丢的 sink 上是矛盾)

❌ 把 Decision Ledger 写成 fire-and-forget(warn 后继续)
   ledger 行必须和业务 mutation 在【同一个 db.batch() 原子事务】里
   (业务改了但 ledger 没记 = 正好制造了「静默丢弃」,见 §5.5 contract ①)

❌ 只存 executionBundleId 的 hash,不存可读 components
   hash 只能追责,不能解释"当时用了啥"。必须同时存 resolved model/params/versions(§3.3)

❌ 让新功能各自拼一套 prompt / 各自记一套 trace
   新输出面(coaching/call 分类)→ 复用同一底座(bundle/Ledger/Traceplane/eval)
   (官方"厚底座薄壳子"——壳子只定义业务目标,底座统一)

这张表的意义:它不要求你现在把所有格子填满。它的作用是——当某个新功能出现时,你不用重新想架构,查表就知道它该插在哪、挂什么 ID、要不要留痕。 这就是「先分层、再填功能」的全部价值。


8. 这和 prompt 分层工作怎么对上

如果当前在改 contacts-analyzer prompt 分层(role / taxonomy-injected / output-contract-injected / field-applicability-policy),对应的是 Data Plane 里 "LLM Request Builder" 那一格的内部结构。

这类改动是需要的,但它只是 Governance Plane → Data Plane 这条线上的一个组件

所以你的担心是对的:如果不先有这张全局图,光改 builder 容易只优化局部。 现在有了图,可以判断:

  • prompt 分层 = Data Plane / Builder 内部整理,继续做
  • 没碰 execution bundle 收敛、Decision Ledger、Traceplane 记结果 —— 这些是下一批独立工作,不应塞进同一个 prompt 分层改动
  • 把 schema 从 prompt 手写挪到 request body(concepts draft Phase 1)= 也是 Builder 内部 ✅ 同 branch 可做

9. 落地路线(和官方 roadmap 对齐,不重复造)

这里只列架构演进的 phase,不是细粒度 issue。每个 phase 是独立可交付单元。

Phase A: Builder 分层
  prompt sections 重组 + schema 移到 request body + taxonomy 从 common 渲染
  → 不碰数据模型,纯 Data Plane / Builder 内部

Phase B: Execution Bundle 收敛(§3)
  handler 入口算 executionBundleId(SHA256 over 已有的 version 原料)
  → 同时存可读 components(resolved model/params/versions),不只存 hash
  → 所有写入点(Neon / Traceplane)带上它
  → 收敛,不造新表

Phase C: Decision Ledger(§5,唯一真缺口)— 先定 contract 再写 schema
  先定 §5.5 四条 contract:① 同 db.batch() 原子事务(不 fire-and-forget)
                          ② 粒度(business-material 进 ledger,debug 归 Traceplane)
                          ③ tenant/storeId scope + PII 边界
                          ④ customer-visible reason taxonomy(enum 映射)
  再补 task_decision_attempts(accepted/rejected/skipped/zero-row + reason + bundle)进 Neon
  → 世界观死规矩"绝不静默丢弃"落地,强一致、可 DB replay
  → 复用 repo 已有 atomic-batch-design 硬约束(ledger = 现有原子写扩一行)

Phase D: Traceplane 记结果(§5.4)
  现有 Traceplane 从"只记 input+proposal"扩成"也记 Writer 裁决结果"
  → 演进现有,不重建
  → 仍是内部 fail-open sink

Phase E(Demo 后): 客户可见性投射
  task_sources evidence 链 + Decision Ledger 投射成 Studio "为什么有/没有这个任务"
  → 产品差异化

Phase F(更后): tool calling / agent(§6)
  capability envelope + approval queue + agent execution log + LLM Gateway
  → 官方 final-state 的"Demo 后"缺口,前提是 A-D 做完

10. 一句话总括

retaintive 不是一条竖直流水线,是三个平面Governance Plane 发版本(resolve execution bundle),Data Plane 跑请求(现在 push context、未来 tool loop),Cross-cutting Plane 全程盯着(trace / audit / idempotency / eval)。Traceability 是三层各司其职:Neon 存被接受的真相、是客户看到的产品面;Decision Ledger 存每条 proposal 的最终命运、是「绝不静默丢弃」的落地、可投射给客户解释「为什么这么判」;Traceplane 存一次 run 的完整 debug 快照、是纯内部 observability、永远不直接面客。给客户展示的是 evidence 链和「为什么这么判」,不是 prompt hash 和 token。 这三层 retaintive 基本都有雏形,唯一真缺口是 Decision Ledger——其余都是「收敛」不是「造轮子」。


11. 调研依据

Verified 2026-06-24 with live rg / Read over callytics-infrastructure + docs repo。

Code anchors:

  • lambda/contacts-analyzer/src/infrastructure/traceplane-publisher.ts — Traceplane ingest(fail-open, redact, recipe vocab)
  • lib/config/environments.ts:25 — "Optional fail-open Traceplane ingest sink" 定性
  • lambda/contacts-analyzer/src/core/prompt-version.ts:15 — promptVersion/taxonomyVersion
  • lambda/contacts-analyzer/src/infrastructure/protocols.ts:160 — aiRunStartedAt human-authority guard
  • lambda/contacts-analyzer/src/infrastructure/neon-repository.ts:748 — contactAnalysisRunId 写入
  • lambda/shared/utils/ai/invoke.ts — generateObject,无 tools(确认无 tool calling)

Doc anchors:

  • docs/product-design/system-worldview.md — 死规矩「绝不静默丢弃」、capability envelope、push→pull 方向
  • docs/ai/product/ai-agent-foundation-final-state.md — 六层基座、Demo 后缺口(approval/agent log/LLM Gateway)
  • docs/ai/product/2026-06-23-llm-application-foundation-concepts.zh.md — 概念链路(本文档补其"三平面"结构缺口)