OpenMed 隐私安全 Agent 运行摘要:openmed.agent.RunSummary确定性元数据设计与边界校验实战
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
导读
OpenMed 是本地优先(local-first)的医疗 AI 平台,其 Agent 编排层在诊疗流程中会执行多次工具调用、产出临床证据与 FHIR/OMOP 资源。为了在仪表盘与审计证据包中呈现"这次运行发生了什么",又绝不让提示词、工具参数、证据文本、文件路径、凭据或异常信息离开数据边界,openmed.agent.RunSummary提供了一套只含确定性元数据的运行摘要机制。读完本文,你将掌握 RunEvent 事件契约、RunSummary.from_events()聚合、from_json()严格解析的完整细节,并能结合实际源码与测试用例,在自己的 Agent 工作流中落地隐私安全的运行审计。
本文主体内容继承自 docs/agent/run-summaries.md,并辅以 openmed/agent/run_summary.py、openmed/agent/outcomes.py 等源码实现与测试证据进行纵深展开。
一、设计目标:只让"元数据"过境,绝不复制内容
openmed.agent.RunSummary的核心承诺是:为仪表盘和证据包生成确定性(deterministic)元数据,但不复制任何敏感内容。在 run_summary.py 的模块 docstring 中明确写着:
Only bounded identifiers, closed workflow outcomes, counts, durations, and artifact digests cross this boundary. Prompts, tool payloads, evidence text, paths, credentials, and exception text are never accepted or rendered.
也就是说,跨越摘要边界的只有五类受限数据:
- 有界的(bounded)工作流标识符;
- 来自闭合词汇表的类型化
WorkflowOutcome(结果类别 + 原因码); - 有界的非负工具调用计数与有限的运行时长;
- 可选的、小写 SHA-256 产物摘要;
- 版本化的 schema 标识符。
而提示词、工具实参、工具输出、证据文本、文件系统路径、凭据、异常文本——一律不被接收、不被渲染。这让运行摘要天然可以进入审计日志、远程仪表盘或证据包,而不触碰 HIPAA 合规所要求的 PHI 边界。
二、事件契约:RunEvent的五个字段
每个运行事件RunEvent(定义见 run_summary.py)只承载以下字段:
| 字段 | 类型 | 约束 |
|---|---|---|
workflow_id | str | 有界标识符,禁止路径或 URL 语法;由正则^A-Za-z0-9?$校验 |
outcome | WorkflowOutcome | 来自闭合结果/原因码词汇表(见第三节) |
tool_call_count | int | 非负整数,上限_MAX_TOOL_CALLS = 10_000_000 |
duration_seconds | float | 有限值,上限_MAX_DURATION_SECONDS = 31_536_000.0(约一年) |
artifact_digests | tuple[str, ...] | 每个事件最多 128 个,格式为sha256:前缀 + 64 位小写十六进制 |
值得注意的校验细节:
- 标识符防注入:测试 test_run_summary.py 明确验证了
"workflow with spaces"、"/tmp/workflow"、"https://example.test"、"../secret"都会被拒绝并抛出invalid_identifier——这正是为了防止路径穿越、URL 语法与自由文本混入摘要。 - 计数与时长有界:
tool_call_count必须是非负整数且不超过上限;duration_seconds先校验类型与范围,再转换为float,并强制math.isfinite,杜绝NaN/Infinity等非有限值。 - 摘要去重:同一事件内的
artifact_digests不允许重复(duplicate_item)。
事件构造示例
from openmed.agent import OutcomeClass, RunEvent, RunSummary, WorkflowOutcome event = RunEvent( workflow_id="clinical-review", outcome=WorkflowOutcome(OutcomeClass.SUCCESS, "completed"), tool_call_count=3, duration_seconds=2.5, artifact_digests=("sha256:" + "a" * 64,), ) summary = RunSummary.from_events([event]) json_payload = summary.to_json() assert RunSummary.from_json(json_payload) == summary markdown_report = summary.to_markdown()三、结果词汇表:WorkflowOutcome的闭合枚举
WorkflowOutcome(见 outcomes.py)是事件中唯一表达"运行结果"的对象,其词汇表是闭合(closed)的——不允许自由文本状态字符串。完整词汇表继承自 docs/agent/outcome-reasons.md:
结果类别(OutcomeClass) | 允许的原因码(reason_code) |
|---|---|
success | completed |
abstained | insufficient_evidence、out_of_scope、low_confidence |
review_required | conflicting_evidence、safety_review、human_gate |
policy_denied | consent_required、purpose_mismatch、phi_policy |
failed | tool_error、timeout、invalid_input |
这张映射表在 outcomes.py 中以_REASON_CODES字典硬编码,并通过allowed_reason_codes()暴露查询接口。失败的关闭原则(fail closed)覆盖以下场景:
- 未知结果类别 →
unknown_class; - 未知原因码 →
unknown_reason; - 类别与原因码不匹配(如
success配timeout)→ 拒绝; - 多余字段 →
unknown_field; - 自由文本原因 → 一律拒绝。
异常信息只命名字段名或稳定错误码(如outcome_class: unknown_class),绝不回显被提交的值。这保证了即使校验失败,也不会把可疑的临床内容泄露进异常堆栈。
to_dict()按固定字段序(schema_version、outcome_class、reason_code)输出;to_json()输出按键排序的紧凑 JSON,保证相同输入序列化后逐字节一致(byte-for-byte identical)——这是审计与去重的基础。
四、聚合:RunSummary.from_events()的边界与确定性
RunSummary.from_events()(见 run_summary.py)把一组RunEvent聚合为一条摘要。源码中的关键逻辑:
- 输入必须可迭代:字符串、bytes、映射对象会被拒绝(
events: invalid_iterable),防止误把单个对象当序列; - 事件数上限
_MAX_EVENTS = 10_000:超过即too_many_items; - 工作流 ID 去重:聚合后
workflow_ids为排序去重的元组,且最多_MAX_WORKFLOWS = 1_024个; - 结果计数:
outcome_counts必然覆盖全部 5 个闭合结果类别,且合计不超过事件上限; - 工具调用累计:
tool_call_count逐事件累加,超限即total_out_of_range; - 时长累计:使用
math.fsum精确求和,避免浮点累加误差,总和超限即拒绝; - 摘要级去重:所有事件的产物摘要合并为排序去重集合,上限
_MAX_SUMMARY_DIGESTS = 4_096。
这种设计保证了"输入有界 + 聚合总量有界",任何异常输入都在进入摘要前被拦截。
直接构造的规范化强制
RunSummary是frozen=True的 dataclass,但其__post_init__(run_summary.py)会强制规范化排序:workflow_ids与artifact_digests必须是"排序去重"后的序列,outcome_counts必须恰好覆盖 5 个类别键。也就是说,绕过from_events()直接构造时,若传入乱序、重复或键不完整的序列,会直接以not_sorted_unique/invalid_keys失败——从构造层面杜绝了非规范化的摘要进入下游。
五、信任边界解析:from_dict()与from_json()
文档明确指出:在信任边界(trust boundaries)应使用RunSummary.from_dict()或RunSummary.from_json()解析摘要。两者都拒绝:
- 缺失字段(
missing_field)与未知字段(unknown_field)——摘要字段是冻结集合_SUMMARY_FIELDS,只有schema_version、workflow_ids、outcome_counts、tool_call_count、duration_seconds、artifact_digests六个; - 不支持的版本(
unsupported_version,必须精确等于openmed.agent.run_summary.v1); - 非法计数(
invalid_count); - 不安全的字符串(
unsafe_string)——字符串要么匹配有界标识符正则,要么匹配sha256:摘要格式,否则拒绝。
from_json()在其之上还有四道额外防线(run_summary.py):
- 大小上限:
MAX_RUN_SUMMARY_JSON_BYTES = 1_048_576(1 MiB),对字符串先按 UTF-8 编码后测长度,超限即json_too_large; - 重复键拒绝:通过
object_pairs_hook=_strict_json_object在解析时检测重复字段并抛duplicate_field; - 非标准非有限数拒绝:
parse_constant=_reject_json_constant拦截NaN/Infinity/-Infinity字面量(non_finite_number); - 畸形 JSON 兜底:解析失败统一转为
summary: invalid_json,且不保留底层源码异常——调用方永远不会看到来自解析器的内部 traceback。
此外,严格解析还会拒绝序列字段中出现映射对象(invalid_sequence,字符串/bytes 也不行),并在转换为 float 之前完成时长边界检查,避免类型混淆带来的精度与安全问题。
六、确定性序列化:to_dict()/to_json()/to_markdown()
摘要的序列化输出是稳定、可复现的:
to_dict():输出前先经过_assert_safe_payload递归校验(run_summary.py),任何非有限浮点数(non_finite_number)、超出白名单的字符串(unsafe_string)、非法类型(forbidden_type)都会触发RunSummaryPrivacyError——这是序列化方向的最后一道隐私闸门;to_json():json.dumps(..., sort_keys=True, separators=(",", ":")),按键排序的紧凑 JSON,保证相同摘要序列化结果逐字节一致;to_markdown():生成确定性 Markdown 报告,固定包含# Agent Run Summary、## Workflows、## Outcomes、## Execution(工具调用数 + 时长)、## Artifacts(SHA-256 列表)五个小节。
JSON 键、结果行、工作流标识符、产物摘要的顺序全部稳定,可直接进入审计比对或证据包生成流水线。同时,摘要层从不读取产物内容或工作流内容——它只搬运元数据,不触碰数据本体。
七、配套生态:内容无关的 Agent 元数据体系
RunSummary不是孤立组件,它与openmed.agent包中的其他隐私安全组件共同构成一套完整的"内容无关元数据"体系(统一导出见 openmed/agent/init.py):
- ArtifactReference:让一次运行指向一个产物,而无需把报告、临床内容、文件名、本地路径或远程 URL 复制进 trace。每个引用只含
art_+ 32 位小写十六进制的透明 ID、闭合类别(evidence/preview/fhir/omop/evaluation)、版本化 schema ID、64 位小写 SHA-256 摘要和不超过有符号 64 位整数上限的正字节数。实现见 artifact_reference.py;批量挂载多个引用时用validate_artifact_references()拒绝重复 ID(duplicate_artifact_id)并保持顺序。重要边界:引用记录的是调用方提供的摘要与大小,并不自行验证;创建、解析、序列化引用永不打开本地文件、不拉取远程资源、不校验临床内容。 - Event Correlation:
run_/act_前缀 + 32 位小写十六进制的 128 位随机关联 ID(RunId/ActionId/ActionCorrelation),运行时用secrets.token_bytes生成,禁止通过对提示词、临床文本、文件名、用户 ID 等做哈希或编码来派生。 - Timing Metadata:调用方提供的单调纳秒边界,
RunTiming/ActionTiming计算精确整数时长,永不读取墙钟,序列化后不含墙钟时间戳、路径或 PHI;父链接必须构成无环图。 - Governance Identifiers:
CapabilityId/PurposeId/PolicyId/WorkflowId/ToolId五种开发者手写名称,遵循<kind>:<reverse-domain>/<local-name>[@<version>]语法,GovernanceIdError的code提供invalid_identifier、wrong_kind、namespace_too_long等稳定诊断。
这些组件的共性设计语言与RunSummary完全一致:闭合词汇、有界长度、规范化解析、错误信息不回显被拒绝的值。整个openmed.agent包在 openmed/agent 目录下均有独立源码模块与对应测试(tests/unit/agent 下的test_run_summary.py、test_outcomes.py、test_artifact_reference.py、test_correlation.py、test_timing.py、test_identifiers.py),可以作为"元数据安全模式"的完整参考实现。
八、测试验证与离线回归
运行摘要的所有边界行为都有离线测试覆盖。以 tests/unit/agent/test_run_summary.py 为例:
test_run_event_accepts_safe_metadata:验证合法元数据被接受且字段保持原值;test_run_event_accepts_closed_outcome_vocabulary:参数化遍历全部 5 个OutcomeClass,验证闭合词汇表;test_run_event_rejects_paths_urls_and_free_text:验证带空格、路径、URL、../的工作流 ID 全部以invalid_identifier拒绝;test_run_event_requires_typed_outcome:字符串或字典形式的 outcome 一律invalid_type拒绝。
全文件共 442 行,覆盖计数越界、时长越界、重复摘要、乱序 ID、未知字段、重复 JSON 键、超大文档、非有限数等负面路径。类似地,治理标识符的语法测试可用如下命令聚焦运行:
uv run --frozen --extra dev pytest tests/unit/agent/test_identifiers.py -q九、落地建议:何时、在何处使用RunSummary
结合源码设计与测试覆盖,以下实践路径可以直接复用:
- 在 Agent 工作流出口生成摘要:每次工作流结束后,用
RunEvent记录工作流 ID、WorkflowOutcome、工具调用数、时长与产物摘要,再通过RunSummary.from_events()聚合。注意workflow_id使用有界标识符(字母数字 +_/./-),不要塞入路径或 URL。 - 在信任边界用
from_json()解析:任何来自外部系统、网络请求或持久化存储的摘要,一律走from_json(),其 1 MiB 上限、重复键检测、非有限数拦截与字段白名单可防御畸形或恶意载荷;不要直接用RunSummary(...)构造去解析不可信数据。 - 摘要只进证据包,内容留在原地:产物本体通过 ArtifactReference 以
art_透明 ID + SHA-256 引用,运行摘要只携带摘要层元数据;两侧都永不读取产物内容。 - 错误处理只看错误码:
RunSummaryError/RunSummaryPrivacyError/OutcomeError的code与field_name是稳定诊断接口,异常消息不回显提交值,可直接用于监控告警而无需担心 PHI 泄露。
这套机制把"Agent 可观测性"与"医疗数据隐私"解耦:仪表盘获得的是确定、有界、无内容的运行事实,审计证据包获得的是可复现、可比对的元数据轨迹,而 PHI 始终停留在本地数据边界之内。
参考与深入阅读
- 主文档:docs/agent/run-summaries.md
- 结果词汇表:docs/agent/outcome-reasons.md
- 产物引用:docs/agent/artifact-references.md
- 关联标识:docs/agent/event-correlation.md、docs/agent/timing-metadata.md、docs/agent/governance-identifiers.md
- 源码实现:openmed/agent/run_summary.py、openmed/agent/outcomes.py、openmed/agent/artifact_reference.py
- 测试用例:tests/unit/agent/test_run_summary.py 及 tests/unit/agent 目录下其余组件测试
【免费下载链接】openmedLocal-first healthcare AI: clinical NER & HIPAA PII de-identification that runs 100% on-device. 2,200+ medical models, 21 languages, Apple MLX + Python, no cloud, no patient data leaving your network. Apache-2.0项目地址: https://gitcode.com/GitHub_Trending/ope/openmed
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考