BodySense Production Agent 从推理到运行时的完整链路
将 BodySense L1 Diagnosis、L2 Treatment、L3 Consultation 与 L4 Async/RAG 串成一条 production Agent 心智链:从 typed reasoning、证据与治理,一直到 action authority、durable runtime、stream contract 和异步资源边界。
[!info] related notes
- 所属 MOC: BodySense 项目 MOC
- L1 深入: BodySense Diagnosis 的 PydanticAI 执行边界
- L3/Python 并发基础: Python 异步编程 · asyncio 任务、超时与取消
- L4 深入: BodySense Python Async / RAG Engineering
- RAG: RAG 知识库设计 · RAG Grounding / Faithfulness 校验
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 = proposer,Runtime = recorder/verifier,Policy = 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 的核心心智已经比较完整:
- constructor DI 和 PydanticAI deps 为什么不是同一种 DI?
- 为什么 exact BodyState revision 是 replay / freshness 的前提?
- 为什么 Python 不应该生成 durable Diagnosis/Treatment identity?
- ToolCallPart 和 structured output tool 有什么区别?
- User information EvidenceGap 为什么不能用 external RAG 解决?
- EvidenceBudget 和 Gap Resolution 为什么必须分开?
- 为什么 promotion unit 应该是 immutable Agent Configuration,而不是 model name?
- Infrastructure fallback 与 business abstain/escalate 有什么不同?
- Non-inferiority 为什么需要预声明 margin?
- SafetyEnvelope 和 DecisionAuthority 分别解决什么问题?
- 为什么
Correct answer != valid execution? - DiagnosisAnalysis 与 BodyStateHypothesis 为什么需要不同生命周期?
- Historical replay、counterfactual replay、current re-analysis 有什么区别?
- Treatment Agent 为什么永远 proposal-only?
- 为什么 Treatment acceptance 要重新检查 freshness/safety?
- 为什么 accepted revision 不应该被原地修改?
- Outcome 为什么必须绑定具体 intervention/revision identity?
- Association 为什么不能直接升级为 causation?
- 为什么 JSON.parse 后的数据仍然应该是 unknown?
ReadableStream.read()的 chunk 为什么不等于一条 event?- Active Turn state 和 historical server state 为什么应该分开?
seq如何支持 idempotency/replay?- Transport disconnect 为什么不一定等于 cancellation?
- Resume 为什么不能随便切到当前最新 Agent Configuration?
async def为什么不能证明内部不 blocking?- sync psycopg 和 local transformer 分别属于什么阻塞类型?
to_thread为什么还需要 concurrency limit?- AsyncConnectionPool 的 pool / connection / transaction 生命周期各是什么?
- Retrieved / Admissible / Resolved 三层如何区分?
- Citation 与 Provenance 有什么区别?
- 为什么 embedding similarity 不能单独证明 Grounding?
- Grounding supported 为什么仍不等于 Treatment execution authorized?
这 32 个问题基本覆盖了 BodySense 从“会写 Agent”到“会设计 Production Agent Runtime”的关键跃迁。