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)钉到正确的平面上,然后回答四个你一直在问的问题:
- execution bundle 是什么、在哪一层 resolve(§3)
- ID(
requestId / aiRunId / contactAnalysisRunId / promptHash)怎么穿过全链路(§4)
- traceability 三层(Neon / Decision Ledger / Traceplane)各负责什么、给客户展示什么(§5)—— 这是最关键的一节
- 未来加 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」——这是错的。
所以你的 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 怎么读这张图(三句话)
- Governance Plane 在上面,它不碰单条请求,只「发版本」。它 resolve 出一个 execution bundle 注入到 Data Plane。改 prompt/model/schema = 改 Governance Plane,必须走 eval + 发布。
- Data Plane 在中间,是请求真正的路径。retaintive 现在是「push context」单向流,未来加 tool calling 会让它变成循环(§6),但 Runtime Control Gate 那一关永远不变。
- Cross-cutting Plane 包住全部。Traceplane、Policy Guard、idempotency、eval 不是某一道工序,是贯穿全程的横切关注点。
⚠️ 命名澄清(避免和官方六层文档冲突):官方 ai-agent-foundation-final-state 把 Policy 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 这张图怎么对上官方六层基座
所以这张图不是另起炉灶——它是把官方六层"侧过来看":六层是数据上下游,三平面是「谁管版本 / 谁跑请求 / 谁全程盯着」。
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 表。第一步只是:
- 把现在散落的 promptVersion / promptHash / model / taxonomyVersion / schemaName 在 handler 入口算成一个
executionBundleId(SHA256 over 这些值)。
- 同时把可读的 components 一起记下来,不能只记 hash(见下方关键点)。
- 把这个 id + components 跟着
contactAnalysisRunId 一起,写进 Neon 写入路径 + Traceplane + Decision Ledger。
- 这样任何一条 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 三层对照表
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。
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 放置规则表(常见新功能 → 加在哪)
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 — 概念链路(本文档补其"三平面"结构缺口)