BodySense Production Agent 从推理到运行时的完整链路

将 BodySense L1 Diagnosis、L2 Treatment、L3 Consultation 与 L4 Async/RAG 串成一条 production Agent 心智链:从 typed reasoning、证据与治理,一直到 action authority、durable runtime、stream contract 和异步资源边界。

#type / synthesis #status / growing #tech / ai #tech / architecture #tech / dev / backend #resource / bodysense #resource / agent

[!info] related notes

BodySense Production Agent 从推理到运行时的完整链路

这篇笔记解决什么问题

BodySense 的 L1~L4 不是四组互相独立的知识点。

它们实际上回答一组层层升级的问题:

L1 Diagnosis
“Agent 怎样得到一个可以被系统认真对待的 reasoning result?”

L2 Treatment
“当 reasoning 开始产生会改变用户行为的 intervention proposal 时,
谁有权让它变成真实行动?”

L3 Consultation
“当 Agent 不再是一次函数调用,而是长时间、多轮、可中断、可恢复的运行时,
状态、事件、流式 UI 和 durable history 怎样保持一致?”

L4 Async / RAG
“支撑这些 Agent 的 DB、Embedding、Retrieval、Evidence 链,
怎样在并发下仍然不阻塞、可追溯、可验证?”

所以完整学习路线不是:

PydanticAI
-> Treatment
-> SSE
-> asyncio

而是:

Typed reasoning
-> Evidence
-> Governance
-> Decision authority
-> Durable domain
-> Action proposal
-> Acceptance / execution authority
-> Outcome feedback
-> Multi-turn runtime
-> Stream protocol
-> Replay / cancellation
-> Async resource boundary
-> Grounding

最终得到的是一套 production Agent 系统心智,而不是几个框架 API。

L1 · Diagnosis:从“调用模型”到“证明这次结果有资格进入业务”

L1 的起点:模型调用不是 Agent 架构的核心

最初最容易看到的是:

DiagnosisService
  -> AIService.generate(...)
  -> model
  -> JSON
  -> Pydantic validate

这条链能工作,但它只回答:

“怎样得到一个结构化结果?”

生产系统还必须回答:

  • 模型拿到了哪些 exact inputs?
  • 它能调用哪些 capabilities?
  • 哪些事实由 runtime 确定,不能让模型编?
  • 这次运行使用的是哪套 prompt/schema/tool/policy?
  • 这套配置是否已经被 eval 证明可以上线?
  • 某个具体 case 是否落在自动化安全边界内?
  • 结果为什么被 allow / degraded / abstain / block?
  • 以后怎样 replay 当时的决定?

L1 的真正升级,就是把这些问题逐层补齐。

L1.1 Typed PydanticAI execution boundary

Typed output 的价值不是少写一次 json.loads

旧心智:

model text
-> JSON parse
-> validate
-> application

Typed Agent 心智:

Agent[Deps, Output]
  -> runtime-managed tool loop
  -> structured output contract
  -> Output object

例如:

Agent[
    DiagnosisDependencies,
    DiagnosisAgentOutput,
]

这里同时声明两件事:

这次 run 能依赖什么
这次 run 最终允许产生什么

这比“prompt 里要求返回 JSON”更强,因为 schema 进入 runtime contract。

Application DI 与 Run-scoped deps 必须分层

长期对象依赖:

DiagnosisService
  -> AIExecutor capability

回答:

这个 application service 长期依赖什么能力?

PydanticAI deps:

agent.run(..., deps=DiagnosisDependencies(...))

回答:

这一轮具体 run 可以访问什么输入、能力与 run state?

因此:

constructor DI
≠ Agent run deps

两者解决不同生命周期。

RunContext 是 runtime 给 tool 的桥

模型只决定:

我要调用 search_evidence(query="...")

它不会自己构造:

RunContext[DiagnosisDependencies]

完整关系:

Application creates deps
  -> Agent runtime creates RunContext
  -> tool gets ctx.deps
  -> capability executes

这样 tool 可以读取 exact BodyState revision,也可以把 runtime-known evidence trail 写回 deps。

详见 BodySense Diagnosis 的 PydanticAI 执行边界

L1.2 BodyState revision:为什么 Diagnosis 必须 pin 精确历史输入

Diagnosis 不是对“一个用户”抽象地推理,而是对:

User
+ exact BodyState revision R17
+ exact relevant history
+ exact profile/context

进行一次 analysis。

这意味着:

DiagnosisAnalysis A1 pinned to BodyState R17

后来 BodyState 变成 R18,并不应该回写 A1 让它“看起来像当时就知道 R18”。

因此 historical artifact 要保存当时的 knowledge boundary。

核心不变量:

Past knowledge != current knowledge
Historical truth != current applicability

这为后面的 Treatment freshness、replay 和 provenance 打基础。

L1.3 Go 与 Python 的 durable ownership 为什么必须清楚

Python Agent 很适合:

  • semantic reasoning;
  • candidate proposal;
  • tool selection;
  • evidence interpretation。

但 durable identity 不应该随模型输出漂移。

例如:

analysis_id
candidate_id
BodyState revision
accepted domain transitions

应该由 Go/domain layer 产生和持久化。

Python 可以输出:

candidate draft

但不能自己决定:

“这个就是数据库里的最终 Candidate #123”

这就是:

semantic ownership
vs
durable business ownership

L1.4 Tool Call、Tool Return 与 Structured Output Tool 不要混淆

在 PydanticAI 中可能看到:

ToolCallPart(search_evidence)
ToolReturnPart(search_evidence)
ToolCallPart(final_result)
ToolReturnPart(final_result)

其中:

search_evidence
= runtime 真正执行一个 capability

final_result
= 模型通过 output tool 提交 structured output

这对测试很重要。

错误测试:

“整轮只能有一个 ToolCallPart”

正确测试:

“业务 tool search_evidence 的 call/return contract 是否正确”

也就是锁业务 contract,而不是框架偶然的消息表示。

L1.5 EvidenceGap:把“缺信息”升级成 typed uncertainty

自由文本:

“信息不足”

太弱,因为不知道:

  • 缺的是什么;
  • 为什么影响决策;
  • 去哪里补;
  • 是否安全关键;
  • 最多补几次;
  • 失败后怎么办。

EvidenceGap 将它结构化:

EvidenceGap
├─ identity
├─ source kind
├─ description
├─ rationale
├─ criticality
└─ targeted query(如果适用)

尤其 source kind 决定 acquisition strategy。

User information 不能用 external RAG 补

例如缺:

“用户是否出现夜间痛醒?”

不能 search 一个医学文章后把答案写成:

“有”

正确的是:

ask user / read durable user-owned state

而 external knowledge gap 才适合 RAG。

L1.6 LLM = proposerRuntime = recorder/verifierPolicy = authority

这是 L1 最重要的 production 分层之一。

模型可以提出:

“我需要查某证据”

runtime 才能证明:

“这次 tool call 真的发生了,实际返回 E31/E32”

policy 才能决定:

“E31/E32 是否允许用于决策,以及最终能否 AUTO”

因此不要让模型同时充当:

提出者
+ 记录者
+ 审核者
+ 最终授权者

否则系统几乎没有独立控制面。

L1.7 EvidenceBudget 不是 Evidence Sufficiency

如果设:

最多搜索 2 次,每次最多 5 条

它只表示资源限制。

可能最终:

2 searches exhausted
+ critical gap unresolved

正确结果仍可能是:

abstain / escalate / ask user

而不是:

“预算用完了,所以就算信息够了”

因此:

Budget state
!=
Knowledge sufficiency state

L1.8 Retrieved Evidence、Admissible Evidence 与 Gap Resolution

这三层必须分开:

Retriever 找到
  -> Retrieved

Policy 允许使用
  -> Admissible

语义上足够消除 uncertainty
  -> Resolved

一个高相似度文档可以 retrieved,却因为 source/version/quality 不合格而 inadmissible。

一个 admissible 文档也可能只回答一半问题,所以 Gap 仍 unresolved。

这是后面 L4 Grounding 的直接前置。

L1.9 Immutable Agent Configuration:为什么 promotion unit 不是 foundation model

一个 production Agent 的行为不只由 model 决定。

同一个 model,如果换了:

  • prompt;
  • output schema;
  • tools;
  • evidence policy;
  • decision policy;
  • generation settings;
  • capability requirement;

行为就可能完全不同。

所以 qualification unit 应是:

Immutable Agent Configuration

概念上:

Configuration
├─ logical model policy
├─ prompt revision
├─ schema revision
├─ tool surface revision
├─ evidence policy revision
├─ governance policy revision
├─ decision policy revision
└─ generation settings

然后用 stable fingerprint/configuration ID 绑定。

这让生产能回答:

“到底是哪一整套行为配置通过了评估?”

而不是含糊地说:

“GPT-X 表现不错。”

L1.10 LiteLLM 的角色:Infrastructure routing,而不是 Business authority

当前理想路径:

Business / Agent Configuration
  -> BodySense logical model
  -> LiteLLM gateway
  -> physical provider/model

LiteLLM 适合拥有:

  • provider normalization;
  • provider credential;
  • retry;
  • cooldown;
  • physical fallback;
  • routing telemetry。

但它不应该决定:

这个健康 case 是否允许自动处理

因此要区分:

Infrastructure fallback
= provider A 不可用,换 provider B

Business fallback
= 证据不足/安全边界外,abstain/escalate

二者不能混成一个“fallback”。

L1.11 Qualification:为什么“能跑通”远远不够

生产 Agent 需要的是:

该 Configuration 是否在预定义数据集/安全切片上达到资格?

而不是:

我手工问了三个问题,感觉挺好。

评价至少要包含:

  • development;
  • holdout;
  • regression;
  • challenge;
  • critical safety slices。

确定性 evaluator 可以检查:

  • response contract;
  • status;
  • candidate policy;
  • side effects;
  • exact configuration provenance;
  • tool trace。

语义 evaluator 再处理开放问题。

L1.12 Non-inferiority:Challenger 不一定要“绝对更高分”

如果 Challenger 更便宜、更快、更稳定,可能只需要证明:

质量没有低于可接受 margin
+ critical safety 没有回归

这就是 non-inferiority。

关键不是运行完再看差多少,而是提前声明:

margin
sample/dataset
observation window
hard guardrails

避免事后为了喜欢的结果改规则。

L1.13 Model × Prompt interaction:不要把组合回归简单甩锅给一个因素

如果测试:

Model A + Prompt v7
Model A + Prompt v8
Model B + Prompt v7
Model B + Prompt v8

只有:

Model B + Prompt v8

明显变差,那么最准确的结论可能是 interaction effect。

不能直接说:

Model B 一定差

也不能说:

Prompt v8 一定差

生产 config qualification 必须尊重组合行为。

L1.14 SafetyEnvelope 与 DecisionAuthority

SafetyEnvelope

表示:

这套 Configuration 已经被证据证明,在什么 case boundary 内可以 normal AUTO。

DecisionAuthority

表示:

给定当前 runtime facts,系统最终允许执行什么动作。

例如 outcome:

allow-normal
allow-degraded
abstain
escalate
block

关键:

LLM confidence

DecisionAuthority

模型可以“很自信”,但如果出现:

  • unknown policy revision;
  • malformed safety fact;
  • critical unresolved gap;
  • unsupported case type;

policy 仍应 fail closed。

L1.15 Reasoning Result 与 Authorized Business Result

模型可能输出一个语义上很漂亮的 Diagnosis candidate。

但系统随后发现:

critical safety gap unresolved

那么:

Reasoning Result exists

不等于:

Candidate can be delivered as normal business result

这就是为什么要分:

semantic result
vs
authorized result

否则治理只能变成日志提示,无法真正控制交付。

L1.16 Durable Diagnosis Domain

Durable 不等于 Aggregate Root

很多东西值得永久保存,但不代表都应该承担领域不变量协调责任。

例如:

DiagnosisAnalysis

可以是 immutable historical artifact。

DiagnosisCandidate

属于某次 Analysis 的候选。

BodyStateHypothesis

则更适合表示跨多个 BodyState revision 演化的长期解释实体。

它们生命周期不同,不要因为“都保存数据库”就做成一个扁平 JSON。

新 BodyState 产生新 Analysis

不要:

R17 Analysis A1
-> 用户状态变成 R18
-> 回写 A1 内容

而应:

R17 -> A1
R18 -> A2

历史才可解释。

L1.17 Configuration Provenance 与 Execution Provenance

Configuration provenance 回答:

批准/选择的行为配置是什么?

Execution provenance 回答:

这次实际执行时发生了什么?

即使:

BodyState R17 + Config C2

相同,两次运行也可能因为:

  • provider observation;
  • retrieval result;
  • tool outcome;
  • timeout;

不同而得到不同实际 execution facts。

所以两类 provenance 不能合并成一个 config ID。

L1.18 Historical Replay、Counterfactual Replay、Current Re-analysis

Historical Replay

冻结历史输入/版本
-> 解释当时为什么做出该决定

Counterfactual Replay

同一个 frozen case
-> 换新 Configuration
-> 比较如果当时用新配置会怎样

Current Re-analysis

使用当前 BodyState
-> 创建新的业务 Analysis

它不是 replay,因为业务世界已经变化。

L1.19 Behavioral Contract:生产 Agent 不追求逐 token 一致

合理的 contract 分层:

Hard invariants
  必须稳定

Bounded semantic behavior
  允许一定变化,但在可接受范围

Presentation variation
  可以自由变化

例如:

candidate 数量上限
safety outcome
EvidenceGap policy
configuration provenance

可以是 hard contract。

措辞、句子顺序则通常属于 presentation variation。

因此 replay 不应该要求逐 token 完全一致。

L1.20 Correct answer != valid execution

一个模型可能最终“猜对” Diagnosis,但过程是:

需要 user fact
-> 没问用户
-> 用 external RAG 猜出用户事实
-> 最后刚好猜对

从 production contract 看仍然失败。

因为非法 evidence path 会在别的 case 中制造严重风险。

所以评估不能只看 final answer。

L1.21 Failure Attribution:找第一个 contract violation

排障链:

Input
-> Reasoning
-> Evidence
-> Runtime Facts
-> DecisionAuthority
-> Persistence
-> Delivery

当 production case 出错,先找:

第一个偏离 contract 的层。

而不是默认:

“模型又 hallucinate 了”

可能实际是:

  • 输入 pin 错 revision;
  • retrieval timeout;
  • evidence cross-run;
  • config mismatch;
  • persistence 投影 bug;
  • frontend 展示错旧状态。

这套心智直接连接 L3/L4。

L2 · Treatment:从“语义建议”到“真实 intervention”的 authority boundary

L2 为什么不能只是复制 Diagnosis 架构

Diagnosis 大多产生:

explanation / candidate / uncertainty

Treatment 会产生:

exercise
frequency
load
progression
stop condition
behavior change

这些内容更接近用户真实行动。

因此系统必须更加明确:

Agent proposal authority

Business acceptance authority

Execution authority

L2.1 AI output 永远是 proposal-only

最重要的 protected contract:

AI-generated Treatment
  = proposal

它不能自己:

make current
accept revision
start training
mutate BodyState

这些 transition 必须由 Go/domain authority 执行。

所以 Python 即使输出完整 TreatmentAgentOutput,也只是:

“我建议这样做”

不是:

“系统已经决定用户应该这样做”

L2.2 Treatment 必须 pin 输入 identity

一个 proposal 应该明确依赖:

exact BodyState revision
+ exact DiagnosisAnalysis/Candidate context
+ candidate assessment readiness
+ active safety state
+ immutable Treatment configuration

否则之后无法解释:

这个方案是基于哪个身体状态和哪个 Diagnosis 提出来的?

L2.3 Generation gate 与 Acceptance gate 不是同一个时刻

Generation

创建 proposal 时检查:

  • Diagnosis eligible;
  • freshness;
  • safety;
  • candidate assessment readiness。

Acceptance

用户/业务真的要把 revision 变成 current 时,必须重新检查

  • Diagnosis 仍然 eligible/fresh;
  • safety 没变化;
  • proposal revision 仍处于可接受状态;
  • BodyState 没发生 material drift。

因为两者之间可能隔了分钟、小时甚至更久。

L2.4 Temporal validity:过去正确不等于现在仍可执行

一个 proposal 在 T1 完全正确:

BodyState R20
-> Treatment Revision T3 proposed

用户在 T2 才接受,但此时:

BodyState R21 materially changed

不能简单说:

T3 当时 valid
-> 现在仍 valid

所以 acceptance 要重新授权。

核心:

historical validity
!=
current execution authority

L2.5 Accepted Revision 应保持 immutable

一旦一个 Treatment revision 被接受并影响训练执行,它就成为历史行为依据。

不应该后续直接原地编辑:

T3 accepted
-> 修改 T3 dosage

更合理:

T3 accepted historical revision
T4 new proposed revision
-> re-authorize
-> accept T4

这样 outcome 才能归因到准确 intervention version。

L2.6 Current pointer 与 Acceptance 要事务一致

如果系统:

revision.status = accepted

但 current pointer 更新失败,就会出现双真值。

因此:

accept revision
+ change current Treatment pointer

应该在同一个数据库 transaction 中完成。

同时可加 BodyState revision lock/guard,防止并发状态变化导致 stale acceptance。

L2.7 Treatment DecisionTrace 必须来自真正执行 authority 的同一组 facts

不要先做决定,再从 log“解释”为什么允许。

更可靠:

TreatmentDecisionPolicy(inputs)
  -> deterministic DecisionTrace
  -> outcome allow/deny

如果 allow,持久化这个同一 trace;如果 deny,也记录 reason code。

这样 trace 是 decision 的产物,不是事后故事。

L2.8 Deny-overrides 与 fail closed

Treatment 是 action-sensitive domain,因此 unknown/malformed facts 应更保守。

例如:

  • unknown safety state;
  • unknown decision phase;
  • malformed required field;
  • conflicting safety facts;

不能默认:

“看起来大概没事,继续”

而应 fail closed。

L2.9 EvidenceGap 在 Treatment 中比 Diagnosis 更接近 Action Risk

Diagnosis 的 gap 可能影响:

“哪一个解释更可能”

Treatment gap 可能直接影响:

“某动作是否禁忌、剂量是否安全、什么时候停止”

因此 EvidenceGap acquisition 的结果不仅影响 semantic quality,还可能影响 action authority。

仍然保护:

user_fact -> no external search
external retrieval -> explicit gap + bounded budget

L2.10 Treatment Configuration 也必须 immutable + qualification

不能因为 L1 已经做过 Diagnosis governance,就认为 Treatment 自动继承资格。

不同 role 有不同:

  • prompt;
  • output schema;
  • intervention policy;
  • evidence need;
  • safety slices。

所以 Treatment 需要自己的 configuration identity、qualification dataset、promotion evidence。

L2.11 Challenger 不等于生产 pointer 已切换

一个新 Treatment v2 可能:

qualification 4/4
EvidenceGap suite 5/5
paired comparison no regressions

这只能说明:

eligible for later rollout

不代表:

production default pointer 已切到 v2

Qualification、Promotion、Deployment Pointer 是三件事。

L2.12 Intervention identity 与 Outcome 必须对应准确版本

训练后用户反馈:

改善 / 无变化 / 加重

不能只记录到“这个 Treatment 名字”。

需要知道:

哪一个 TreatmentRevision
哪一个 Intervention identity/version
执行了什么 dosage/progression

否则长期系统无法做可靠 outcome attribution。

L2.13 Association 与 Causation

用户做了 Intervention I 后好转:

I happened before improvement

只证明 association。

可能还有:

  • 自然恢复;
  • 其他训练;
  • 睡眠改变;
  • placebo/expectation;
  • measurement noise。

所以 BodyStateHypothesis 更新要谨慎,不能简单:

做了臀桥后好转
-> 证明某 Diagnosis 为真

Treatment outcome 是证据,但不自动等于因果证明。

L2.14 Closed Loop:Treatment 的价值在反馈,而不只是生成方案

理想闭环:

BodyState
-> DiagnosisAnalysis
-> candidate assessment
-> Treatment proposal
-> acceptance
-> intervention execution
-> Outcome
-> BodyState update
-> Hypothesis update
-> next Diagnosis/Treatment revision

这让系统从“一次 AI 回答”进化为 longitudinal health system。

L3 · Consultation:从一次 Agent 调用到 durable multi-turn runtime

L3 为什么和 Diagnosis/Treatment 不一样

Diagnosis/Treatment 常可以建模成:

input
-> one Agent run
-> typed output

Consultation 是:

multi-turn
+ streaming
+ tool loop
+ ask_user interrupt
+ resume
+ checkpoint
+ disconnect/reconnect
+ frontend active state

因此它更接近 runtime / protocol engineering。

L3.1 Python reasoning runtime 与 Go durable run envelope

North-Star:

Go ConsultationRuntime
  = run envelope / durable identity / idempotency / public SSE

Python ConsultationThread
  = LangGraph reasoning runtime / tools / interrupts / checkpoints

两边都“有状态”,但状态职责不同。

Go owns

  • durable Run identity;
  • business configuration selection;
  • event persistence;
  • idempotency;
  • public run lifecycle;
  • decision/provenance envelope。

Python owns

  • graph node state;
  • semantic tool loop;
  • checkpointed execution state;
  • interrupt/resume continuation。

不要因为 LangGraph 有 checkpoint 就把整个业务 run ownership 迁到 Python。

L3.2 Stream Event 是跨信任边界协议,不只是 TypeScript type

网络读到:

JSON.parse(line)

结果类型是:

untrusted runtime value

即使你写:

const event = JSON.parse(line) as StreamEvent

TypeScript 也只是被你“说服”了,没有进行 runtime validation。

正确边界:

unknown bytes
-> decode
-> JSON.parse = unknown
-> runtime schema validation
-> trusted StreamEvent discriminated union
-> reducer

这体现:

Static type
!=
Runtime trust

L3.3 Discriminated Union 为什么适合 StreamEvent

例如:

type StreamEvent =
  | { type: "text.delta"; seq: number; delta: string }
  | { type: "tool.call"; seq: number; toolCallId: string; ... }
  | { type: "tool.result"; seq: number; toolCallId: string; ... }
  | { type: "interaction.required"; seq: number; ... }
  | { type: "run.completed"; seq: number; ... }

type 是 discriminator:

switch (event.type) {
  case "text.delta":
    // event narrowed
}

编译器可以帮助检查字段与 exhaustiveness。

但前提是 runtime parser 先把外部 unknown 变成 trusted union。

L3.4 Chunk boundary 与 Event boundary 不一样

ReadableStream 给你的网络 chunk 可能:

一个 event 被拆成两个 chunk

也可能:

一个 chunk 里包含多个 events

所以不能假设:

read() 一次 = 一条 JSON

正确流程通常:

Uint8Array chunks
-> TextDecoder streaming decode
-> buffer text
-> split complete record boundary
-> keep incomplete tail
-> parse complete records

这是字节层 protocol 与业务 Event layer 的分离。

L3.5 Reducer 应该是纯状态机,而不是 UI 回调垃圾桶

Active Turn 里可能有:

streaming text
tool calls
citations
pending interaction
completion/error
lastSeq

如果这些分散在多个 useState 和 callback:

  • replay 很难;
  • duplicate event 很难统一处理;
  • tool call/result 容易错位;
  • disconnect/reconnect 很难恢复。

Reducer 让更新变成:

State + Event -> New State + Effects

甚至 effect 可以进一步显式化,保证 reducer 本身纯。

L3.6 seq 是 replay/idempotency contract,不只是排序字段

如果事件有 durable seq

1, 2, 3, 4...

前端可以保存:

lastSeq

然后对:

seq <= lastSeq

忽略重复/过旧事件。

重新连接时:

after_seq = lastSeq

服务器只补后续 durable events。

这让:

live delivery
+ replay

共享同一个事件身份模型。

L3.7 Tool Call 应 upsert,而不是盲目 append

Streaming/tool runtime 可能重复投递、replay 或先 result 后 call 补建。

如果每次:

toolCalls.push(event)

就可能出现重复卡片。

更稳的 key:

tool_call_id

状态机应:

call -> upsert placeholder/details
result -> find by tool_call_id and fill result

如果 result 先来,也可建立 placeholder,保持可恢复。

L3.8 Active Turn State 与 Historical Message State

正在流式生成的一轮 assistant turn 和已经持久化完成的历史 message,不应该混成同一个 mutable state。

可以区分:

Active Turn
  = ephemeral/live projection of current run events

Historical thread messages
  = durable server state projection

完成后:

persisted server state becomes source of truth
-> active turn reset

这样不会长期保留两份会漂移的“当前答案”。

L3.9 TanStack Query 与 UI local state 的边界

TanStack Query 适合:

server-owned state

如:

  • sessions;
  • historical messages;
  • durable run state;
  • BodyState data。

本地 reducer/context 适合:

transient UI projection

如:

  • 当前 streaming delta;
  • 正在显示的 tool progress;
  • input focus;
  • panel open state。

不要把所有 UI state 都塞 Query cache,也不要把 server truth 长期复制进 local state。

L3.10 Transport disconnect 与 Business cancellation 不一定相同

用户浏览器断开 SSE 可能是:

  • 切网络;
  • 页面刷新;
  • 临时断线;

它不一定表达:

“请取消这次 Agent run”

如果把 disconnect 自动当 cancellation,就无法可靠 reconnect/replay。

更清楚的 contract:

transport disconnect
  -> connection ended
  -> run may continue / durable events persist

explicit cancel command
  -> business/runtime cancellation
  -> durable terminal state/event

两者必须显式定义,而不是让 HTTP socket 状态偷偷决定业务状态。

L3.11 AbortController 的范围

前端 AbortController 可以取消:

当前 fetch / stream consumption

但它是否也取消服务器 Agent run,取决于后端协议。

不能因为:

controller.abort()

就想当然地认为:

LangGraph run 也被 durable cancelled

这再次体现 transport cancellation 与 business cancellation 的区别。

L3.12 Interrupt / Resume:继续同一 reasoning thread,但 public run identity 可能不同

LangGraph interrupt 可能让 Python checkpoint 保留一个 thread。

用户回答后 resume:

continue same semantic thread

但 Go 侧可能为不同 public execution segment 维护 run identity。

所以要清楚区分:

LangGraph thread identity
vs
public durable Run identity

不能因为两个都叫 run/thread 就混在一起。

L3.13 Resume 也必须 pin configuration identity

如果中断前:

Consultation Config C15

resume 时 deployment pointer 已变 C16,直接用 current pointer 会导致:

同一条 semantic thread 前半段 C15
后半段 C16

这破坏 replay 与 provenance。

因此 resume 要遵循被中断 run/thread 的 exact configuration identity,而不是当前默认配置。

L3.14 Immutable Consultation Configuration 如何适配 LangGraph

虽然 Consultation 不是 PydanticAI single-shot Agent,仍然可以把 behavior-significant elements 版本化:

consultation configuration
├─ prompt revision
├─ tool surface revision
├─ governance revision
├─ logical model
└─ generation settings

Go 选择 repository-known configuration,Python resolve exact manifest,再构建 LangGraph runtime。

因此 Agent Configuration 是框架无关思想,不只适用于 PydanticAI。

L3.15 Consultation Replay 的三个 drift 层

Counterfactual replay 不应该只比较最终文本是否一样。

可以分:

Hard drift
  违反必须不变的 contract

Semantic drift
  内容/决策意义发生变化

Presentation drift
  措辞/格式变化

例如:

  • tool side effect 多了一次 -> hard drift; -建议重点变化 -> semantic drift; -同义改写 -> presentation drift。

这比 token diff 更符合 Agent 行为评价。

L3.16 Streaming UI bug 往往是 protocol/state bug,不只是“React 渲染慢”

例如重复 tool card 可能来自:

  • event duplicate;
  • seq 没守住;
  • reducer append 而非 upsert;
  • replay 和 live 同时应用;
  • historical/live state 混合。

所以排查顺序应先看:

raw event stream
-> validated event
-> reducer transition
-> selector
-> render

不要第一步就 useMemo/memo 做性能优化。

L4 · Async / RAG:让 Agent Runtime 在并发下仍然成立

L4 已拆成独立详细专题,主入口:

BodySense Python Async / RAG Engineering

这里仅总结它与 L1~L3 的关系。

L4.1 Async Façade 不是 Non-blocking

async def search():
    cur.execute(...)   # sync DB

仍然 block Event Loop。

async def generate():
    model.encode(...)  # sync CPU/GPU

也一样。

所以 async review 必须追到真实 resource boundary。

L4.2 Async Postgres Pool 保护 Consultation 并发

如果 RAG search 使用 sync psycopg,一个慢查询可能阻塞:

  • 另一个 Agent run;
  • SSE;
  • health;
  • interrupt/resume。

North-Star 是 lifespan-owned AsyncConnectionPool。

详见 BodySense KnowledgeLibrary 的 Async Postgres Pool

L4.3 Local Embedding 需要 bounded executor

Remote API:native async I/O。

Local transformer:同步 CPU/GPU。

所以 local encode() 应 offload,并限制并发,防 CPU/GPU oversubscription。

详见 BodySense Embedding 的 Async 执行边界

L4.4 Targeted RAG 把 L1 EvidenceGap 接到真实 retrieval plumbing

EvidenceGap
-> bounded acquisition
-> KnowledgeLibrary
-> normalized Evidence
-> provenance/admissibility
-> gap resolution

详见 BodySense Targeted RAG 与 Evidence Provenance

L4.5 Grounding 把 L2 Treatment claim 接回 Evidence

不是:

动作名字出现过 -> faithful

而是:

Intervention material claim
-> exact run-local admissible evidence
-> deterministic provenance
-> semantic support
-> optional Judge

详见 RAG Grounding / Faithfulness 校验

四个阶段放到同一个 Production Failure 中理解

假设用户最终收到一个不合适 Treatment。

只说:

“LLM hallucinated”

几乎没有诊断价值。

完整链路应该检查:

L1:Reasoning / Evidence / Governance

  • exact BodyState revision 对吗?
  • Configuration ID 对吗?
  • EvidenceGap source kind 对吗?
  • external RAG 是否伪造 user fact?
  • Evidence provenance 对吗?
  • DecisionAuthority 是否允许?

L2:Action Authority

  • proposal 是否被错误直接 make current?
  • acceptance 时有没有重新检查 freshness/safety?
  • BodyState 是否已经变化?
  • accepted revision 是否 immutable?
  • intervention dosage 是否属于该 revision?

L3:Runtime / Protocol

  • 前端看到的是当前 run 还是旧 replay?
  • seq 是否重复应用?
  • resume 是否换了 configuration?
  • transport disconnect 是否误触发 cancel?
  • durable event projection 是否正确?

L4:Infrastructure / RAG

  • DB 是否阻塞导致 evidence acquisition timeout?
  • local embedding 是否阻塞 loop?
  • pool 是否耗尽?
  • Evidence 是否真的 admissible?
  • Grounding 是否只匹配了动作名却漏掉剂量?

最终要找的是:

第一个 contract violation。

这就是 L1~L4 合起来的 production debugging method。

一组贯穿四层的“不等式”

这些不等式非常值得直接记忆:

async def
!=
non-blocking execution

retrieved evidence
!=
admissible evidence

evidence budget exhausted
!=
gap resolved

citation exists
!=
claim grounded

LLM confidence
!=
decision authority

reasoning result
!=
authorized business result

proposal generated
!=
proposal accepted

proposal accepted at T1
!=
execution still authorized at T2

historical truth
!=
current applicability

association
!=
causation

static TypeScript type assertion
!=
runtime validation

transport disconnect
!=
business cancellation

LangGraph thread identity
!=
public durable Run identity

configuration provenance
!=
execution provenance

correct final answer
!=
valid execution path

只要这些边界没有混淆,BodySense 的复杂度就会变得清晰很多。

如何判断一个状态应该归谁拥有

遇到新状态时可以依次问:

1. 它是用户长期业务事实吗?

例如 BodyState。

倾向:Go durable domain owner。

2. 它是一次 Agent run 的临时 reasoning state 吗?

例如 tool loop intermediate state。

倾向:Python runtime / LangGraph/PydanticAI。

3. 它是模型输出的 semantic proposal 吗?

例如 DiagnosisCandidateDraft / Treatment proposal。

模型提出,domain layer 验证/赋 durable identity。

4. 它是 runtime 已知事实吗?

例如:

实际调用了哪个 tool
实际返回了哪个 evidence_id

runtime 记录,不让模型“声明”。

5. 它是 authority decision 吗?

例如:

是否 allow acceptance

deterministic domain policy 拥有。

6. 它是 UI 的 transient projection 吗?

例如当前 streaming text/tool progress。

React Active Turn state 拥有;完成后回归 server truth。

这个问法比按语言分:

“Python 管 AI,Go 管后端,React 管页面”

更精确。

如何判断一个逻辑应该 deterministic 还是交给 LLM

适合 deterministic

  • ID equality;
  • enum legality;
  • configuration identity;
  • evidence 是否实际被 retrieved;
  • source/run/version ownership;
  • budget count;
  • safety deny-overrides;
  • freshness/revision comparison;
  • state transition legality;
  • required field;
  • seq/idempotency;
  • transaction guard。

适合 LLM / semantic evaluator

  • 症状语义归纳;
  • candidate hypothesis generation;
  • natural language explanation;
  • 某 Evidence 是否在语义上支持 nuanced claim;
  • 信息是否在自然语言上存在冲突;
  • open-ended comparison。

核心原则:

机器已经确定知道的事实,不要再让模型猜;只有真正需要语义判断的部分才交给模型。

如何判断一个任务应该 async I/O、thread、process 还是 durable Job

Remote HTTP / DB wait
-> native async I/O

短同步 SDK / local transformer
-> bounded thread/executor

重纯 Python CPU、多核有价值
-> process/native strategy

长时间、可恢复、需要 progress/retry
-> durable Job Runtime

不要因为“异步”就全部上队列,也不要因为“Python 有 GIL”就把所有工作都丢进 process pool。

学完 L1~L4 后的最小架构图

                    ┌────────────────────────────┐
                    │ Go Durable Domain / Policy │
                    │ BodyState / Run / Authority│
                    └─────────────┬──────────────┘
                                  │ exact inputs/config

                    ┌────────────────────────────┐
                    │ Python Agent Runtime       │
                    │ PydanticAI / LangGraph     │
                    │ semantic reasoning/tools   │
                    └─────────────┬──────────────┘
                                  │ EvidenceGap/tool

                    ┌────────────────────────────┐
                    │ Async RAG Resource Layer   │
                    │ async PG pool              │
                    │ bounded embedding executor │
                    └─────────────┬──────────────┘
                                  │ normalized evidence

                    ┌────────────────────────────┐
                    │ Evidence/Grounding Policy  │
                    │ provenance/admissibility   │
                    │ semantic support           │
                    └─────────────┬──────────────┘
                                  │ reasoning result

                    ┌────────────────────────────┐
                    │ Deterministic Authority    │
                    │ allow/degrade/abstain/...  │
                    └─────────────┬──────────────┘
                                  │ durable events/results

                    ┌────────────────────────────┐
                    │ Stream / Replay Contract   │
                    │ seq + validated StreamEvent│
                    └─────────────┬──────────────┘

                    ┌────────────────────────────┐
                    │ React Active Turn / UI     │
                    │ transient projection       │
                    └────────────────────────────┘

这个图比“React + Go + Python 三层架构”更接近 BodySense 真正需要保护的边界。

最终复习问题

如果以下问题都可以不看答案讲清楚,L1~L4 的核心心智已经比较完整:

  1. constructor DI 和 PydanticAI deps 为什么不是同一种 DI?
  2. 为什么 exact BodyState revision 是 replay / freshness 的前提?
  3. 为什么 Python 不应该生成 durable Diagnosis/Treatment identity?
  4. ToolCallPart 和 structured output tool 有什么区别?
  5. User information EvidenceGap 为什么不能用 external RAG 解决?
  6. EvidenceBudget 和 Gap Resolution 为什么必须分开?
  7. 为什么 promotion unit 应该是 immutable Agent Configuration,而不是 model name?
  8. Infrastructure fallback 与 business abstain/escalate 有什么不同?
  9. Non-inferiority 为什么需要预声明 margin?
  10. SafetyEnvelope 和 DecisionAuthority 分别解决什么问题?
  11. 为什么 Correct answer != valid execution
  12. DiagnosisAnalysis 与 BodyStateHypothesis 为什么需要不同生命周期?
  13. Historical replay、counterfactual replay、current re-analysis 有什么区别?
  14. Treatment Agent 为什么永远 proposal-only?
  15. 为什么 Treatment acceptance 要重新检查 freshness/safety?
  16. 为什么 accepted revision 不应该被原地修改?
  17. Outcome 为什么必须绑定具体 intervention/revision identity?
  18. Association 为什么不能直接升级为 causation?
  19. 为什么 JSON.parse 后的数据仍然应该是 unknown?
  20. ReadableStream.read() 的 chunk 为什么不等于一条 event?
  21. Active Turn state 和 historical server state 为什么应该分开?
  22. seq 如何支持 idempotency/replay?
  23. Transport disconnect 为什么不一定等于 cancellation?
  24. Resume 为什么不能随便切到当前最新 Agent Configuration?
  25. async def 为什么不能证明内部不 blocking?
  26. sync psycopg 和 local transformer 分别属于什么阻塞类型?
  27. to_thread 为什么还需要 concurrency limit?
  28. AsyncConnectionPool 的 pool / connection / transaction 生命周期各是什么?
  29. Retrieved / Admissible / Resolved 三层如何区分?
  30. Citation 与 Provenance 有什么区别?
  31. 为什么 embedding similarity 不能单独证明 Grounding?
  32. Grounding supported 为什么仍不等于 Treatment execution authorized?

这 32 个问题基本覆盖了 BodySense 从“会写 Agent”到“会设计 Production Agent Runtime”的关键跃迁。

创建于 2026/8/23 更新于 2026/8/23