在构建复杂 Agent 工作流(Multi-Agent Workflow)时,开发者面临的最严峻挑战往往不在于 LLM 的推理逻辑,而在于数据流(Data Flow)与工具链(Tool Chain)的越权与泄露。
在一个标准的自动化退款工作流中,extractor 节点解析出用户的退款原因与摘要;下一个负责"CRM 查询"的 crm-agent 需要使用初始输入中的真实 Token 和邮箱调用外部接口,而再下一个负责"最终审核"的 reviewer 节点,可能只需要知道退款摘要。
- Token 和邮箱会不会直接暴露在
crm-agent的 Prompt 里? reviewer会不会无意中读取到了上游敏感的客户联系方式?- 工具调用返回了用户的明文数据,会不会又通过模型回复或日志泄露出去?
传统做法依赖 Prompt 提示词告诉模型"不要泄露",这在工程上是不可靠的。作为一款基于 Tauri + Rust 构建的桌面自动化工具,Workrun 的策略是:用系统架构(Architecture Constraints)代替 Prompt 约束。我们将数据分为"模型可见"与"工具可用"两套视图,并在输入、输出、工具执行与日志等边界上补齐强硬的 Guardrails。
场景引入:一个真实的退款工作流
为了理解 Workrun 的安全机制,我们来看一个具体的退款工作流:

scss
[用户提交退款申请]
│
▼
[提取节点 extractor] ──(提取基本信息与路由)──► [CRM 节点 crm-agent] ──► [审核节点 reviewer]
1. 工作流初始输入 (Workflow Input)
JSON
perl
{
"orderId": "20260903001",
"customerEmail": "alice@example.com",
"crmToken": "Bearer eyJhbGciOi..."
}
2. extractor 节点的原始输出
extractor 作为一个 LLM 节点,由于 Agent 看到的本身就是脱敏后的输入(customerEmail 显示为 [EMAIL REDACTED]),其生成的输出结构如下:
JSON
css
{
"summary": "用户申请订单 20260903001 的退款",
"customer": {
"email": "[SENSITIVE REDACTED]",
"reason": "重复下单"
},
"internalScore": 82
}
我们的目标是:
crmToken与customerEmail绝对不能注入到任何 Agent Prompt 中;crm-agent执行 CRM 工具时,必须能够通过受控方式拿到初始输入里的真实crmToken与customerEmail;reviewer节点只能读取到summary,绝对看不到internalScore或敏感联系方式。
下面是 Workrun 如何在配置层与运行时实现这一目标。
Scope State:数据隔离与双轨状态
Workrun 拒绝把工作流 State 当成一份所有节点平铺共享的 JSON。每个节点都有自己的 State Namespace,节点输出默认私有,按字段控制暴露边界。
scss
┌───────────────────────────────┐
│ Workflow Raw State │ (包含初始 raw input,仅在内存中保存)
└──────────────┬────────────────┘
│
[PII & Sensitive Redaction]
│
▼
┌───────────────────────────────┐
│ Workflow Visible State │ (用于 Agent Prompt/UI/日志)
└──────────────┬────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LLM Agent Prompt │
└─────────────────────────────────────────────────────────────────────────────┘
1. 工作流输入层配置 (Workflow Settings)
首先在工作流级别的输入配置(inputSchema)中,标记敏感字段与授权节点:
JSON
json
{
"inputSchema": {
"fields": [
{ "key": "orderId", "type": "string" },
{ "key": "customerEmail", "type": "string" },
{ "key": "crmToken", "type": "string" }
],
"sensitiveFields": ["crmToken", "customerEmail"],
"rawReaders": ["crm-agent"]
}
}
2. 节点层配置 (extractor Inspector)
在 extractor 节点中,我们可以进行如下配置(在 UI 的 Inspector 面板中操作):
JSON
json
{
"id": "extractor",
"kind": "agent",
"data": {
"state": {
"access": {
"readers": ["crm-agent"], // 允许读取该节点 State 的下游节点
"rawReaders": ["crm-agent"] // 允许使用原始数据的节点
},
"sensitiveFields": ["customer.email"], // 强制脱敏的业务字段
"globalKeys": ["summary"] // 发布到全局 Global State 的字段
}
}
}
这段配置在运行时会产生极严密的隔离效果:
- 默认私有与全局发布 :
internalScore和customer留在extractor私有作用域中。只有summary会发布到 Global State。下游的reviewer因为不在readers列表中,它完全读不到extractor的私有 State,只能读取已发布的summary。 - Prompt 永远只看 Visible State :即便
crm-agent被授权在readers中,其 Agent 绑定的 Prompt 拿到的 Context 中,敏感字段依然是脱敏状态:
JSON
json
// crm-agent 的 Prompt 实际收到的 Context
{
"orderId": "20260903001",
"customerEmail": "[SENSITIVE REDACTED]",
"crmToken": "[SENSITIVE REDACTED]",
"summary": "用户申请订单 20260903001 的退款"
}
Tool State Binding:原始值不进 Prompt,只在工具执行前恢复
既然 Prompt 里只有 [SENSITIVE REDACTED],crm-agent 的 CRM 查询工具该如何拿到真实数据?
Workrun 实现了 Tool State Binding(工具状态绑定) 机制。在 crm-agent 节点的配置中,用户可以将拥有原始读取权限的 State 路径(如 Workflow Input 中的真实字段)显式映射到工具参数上:
JSON
json
{
"toolStateBindings": [
{
"toolId": "crm.lookup",
"argumentPath": "email",
"statePath": "customerEmail" // 指向工作流初始输入中的原始邮箱字段
},
{
"toolId": "crm.lookup",
"argumentPath": "token",
"statePath": "crmToken" // 指向工作流初始输入中的原始 Token 字段
}
]
}
当工作流运行到工具调用阶段时,数据的生命周期经历了如下蜕变:
perl
1. LLM 推理生成参数 ──► { "email": "[SENSITIVE REDACTED]", "token": "[SENSITIVE REDACTED]" }
│
2. ManagedTool 拦截 ──► 检查节点是否拥有针对该 statePath 的 `rawReaders` 授权?
│
┌───────────────────┴───────────────────┐
[已授权] [未授权]
│ │
根据 AST 路径在 `raw_state` 保持 `[... REDACTED]` 占位符,
中查找真实值并精准替换 ManagedTool 拒绝执行并硬阻断抛错
│
▼
3. 最终发给 CRM 接口 ──► { "email": "alice@example.com", "token": "Bearer eyJhbGciOi..." }
图中包含 Global State、节点独立 namespace、Runtime State,以及 Visible State 与 Tool State Binding 的边界关系。

演示视频

其中 CRM Agent 调用的"模拟 CRM 查询" Tool 代码如下:
python
"""Tool App entrypoint.
Workrun validates the Agent arguments against this App's input fields, then invokes the decorated function. Return a JSON object that matches the configured output fields.
"""
from workrun_sdk.tool import tool
@tool(
name="mock_crm_lookup",
description="查询模拟客户的退款资格。",
)
def mock_crm_lookup(email: str, token: str) -> dict[str, object]:
# 该工具只用于演示;不记录也不返回传入的令牌。
if not token:
raise ValueError("缺少 CRM 演示令牌")
# 仅用于本地演示:Tool output 会显示这两行日志。
print("token: ", token); // 查看是否获取到原始的 crmToken
print("email: ", email); // 查看是否获取到原始的 customerEmail
return {
"status": "active",
"customerName": "Alice",
"email": email,
"refundEligible": True,
}
关键控制限制
- 严格按路径替换 :绑定仅替换模型已经显式生成的参数路径 (如
email),系统绝不会自动将整份raw_state塞进请求包里。 - 占位符防逃逸 :如果工具准备发出一瞬间,参数中依然包含
[... REDACTED],说明授权失败或路径匹配失败。Workrun 会直接拒绝调用,绝不会把占位符作为请求发送给外部 Process 或 MCP 工具。
Guardrails:边界上的硬核防线
如果说 Scope State 是可配置的数据隔离规则,那么 Guardrails 就是 Workrun 内置在 Agent 执行器层面的硬规则策略。
scss
[用户 Prompt] ──► [ 输入 Guardrail ] ──► [ LLM 模型 ]
│
[外部 API / 工具] ◄── [ 工具 Guardrail ] ◄──┤ (模型发出 Tool Call)
│ │
└────────► [ 输出 Guardrail ] ──► [ UI / 事件 / 日志 ]
1. 输入 Guardrail(硬阻断 vs 脱敏)
当用户输入进入模型前,引擎会进行扫描:
- 常规 PII 脱敏 :邮箱、电话、IP 地址、中国手机号、身份证号等转换为
[REDACTED]后放行; - 认证凭证直接阻断 :一旦检测到 Bearer Token、OpenAI
sk-Key、GitHub/AWS Token 或 PEM 私钥,系统策略不是"替换后放行",而是直接拒绝执行(Input Error) 。
工程逻辑:用户把 Token 粘贴进 Prompt 往往是误操作。如果脱敏后放行,模型通常会因缺失密钥而产生无效对话,直接阻断能最快给用户明确的安全反馈。
2. 输出 Guardrail(阻断旁路泄漏)
如果 CRM 工具为了业务需要拿到了原始值,并返回了包含明文邮箱的数据:
JSON
json
// CRM 工具返回给 Agent 的原始数据
{ "customerName": "Alice", "email": "alice@example.com", "ip": "10.0.0.8" }
输出 Guardrail 会在工具结果写回 Agent 上下文、工作流日志以及前端 UI 事件前再次介入,将数据强制清洗为:
JSON
json
{ "customerName": "Alice", "email": "[EMAIL REDACTED]", "ip": "[IP ADDRESS REDACTED]" }
这彻底杜绝了"工具成功拿到秘密,但通过结果回显给模型/日志"的旁路泄漏路径。
3. 流式输出(Streaming)防泄露
流式输出(SSE Chunk)是脱敏引擎极易破防的场景。如果模型将 Token 拆成分块发送:
Chunk 1: "sk-" -> Chunk 2: "12345" -> Chunk 3: "67890"
单个 Chunk 无法匹配正则,明文就会直接暴露在前端。
Workrun 在 Rust 后端(agent.rs)的策略是:需要经过输出 Guardrail 的 Agent 消息会在后端做 Chunk 汇聚缓存,待完整消息形成并脱敏后统一推送到 UI 渲染;文本与 JSON 数据流则在 Rust 结构体层走递归过滤,封堵流式泄露。
UI 配置与实际可见效果对应
为了给开发者提供直观的心理模型,Workrun 中的配置项与运行时的实际表现有着严格的映射关系:
| 想解决的问题 | Workrun 配置位置 | 运行时效果 | UI 呈现位置 |
|---|---|---|---|
| 不希望所有节点看到输入中的 Token | inputSchema.sensitiveFields: ["crmToken"] |
Prompt 中 crmToken 变成 [SENSITIVE REDACTED] |
Workflow Settings 输入字段旁打开 Sensitive 开关 |
| CRM 工具必须使用真实 Token | inputSchema.rawReaders: ["crm-agent"] |
工具执行阶段拿到原值,模型 Prompt 仍保持脱敏 | Workflow Settings 的 Raw input readers 勾选 crm-agent |
extractor 客户资料不默认共享 |
不配置 globalKeys |
customer 等字段保留在 extractor 私有 State 中 |
节点 Inspector 的 State access |
| CRM 节点读取客户资料 | readers: ["crm-agent"] |
crm-agent 可读取 extractor 的可见 State |
节点 Inspector 的 Allowed readers |
| CRM 工具需要真实邮箱与 Token | rawReaders: ["crm-agent"] + Tool State Binding |
模型看脱敏邮箱,工具执行前绑定并替换成初始输入的真实值 | 节点 Inspector 的 Raw state readers + Tool State Bindings |
| 审核节点只需要摘要 | globalKeys: ["summary"] |
仅 summary 发布到 Global State |
节点 Inspector 的 Global State publication |
在大盘展示(Workflow State 面板)中,Workrun 展示的永远是 Visible State。即便工具在幕后静默使用过真实凭证,开发者在 UI 和工作流日志中看到的永远是经过脱敏后的优雅视图。
如果你想尝试,可以动手复现:在 Workrun 配置"退款安全工作流" Workrun 仍在开发阶段,暂未提供工作流及关联配置的一键导入、导出能力。想亲自验证 Scope State、Tool State Binding 和 Guardrails 的效果,可以本地运行 Workrun,并按下面步骤手动配置。
示例仅使用演示邮箱和 Token,不要打印或填入生产环境密钥。
1. 创建工作流和输入字段
新建一个 Task 工作流,名称设为「退款流程安全演示」。
添加三个输入字段:
| Key | 类型 | 必填 | 敏感字段 |
|---|---|---|---|
orderId |
string | 是 | 否 |
customerEmail |
string | 是 | 否 |
crmToken |
string | 否 | 是 |
在 Input Schema 的 Raw readers 中,仅授权「CRM 查询」Agent。这样,CRM 工具可以在执行时拿到原始邮箱和 Token,但其他 Agent 的提示词仍只会看到脱敏后的 State。
2. 创建 Tool App
创建一个 Tool App,名称为「模拟 CRM 查询」,配置两个输入参数:
email:string,必填token:string,必填
输出 Schema:
json
{
"type": "object",
"properties": {
"status": { "type": "string" },
"customerName": { "type": "string" },
"email": { "type": "string" },
"refundEligible": { "type": "boolean" }
},
"required": ["status", "refundEligible"]
}
工具代码如下:
python
from workrun_sdk.tool import tool
@tool(
name="mock_crm_lookup",
description="查询模拟客户的退款资格。",
)
def mock_crm_lookup(email: str, token: str) -> dict[str, object]:
if not token:
raise ValueError("缺少 CRM 演示令牌")
# 仅用于本地演示:Tool output 会显示这两行日志。
print("token:", token)
print("email:", email)
return {
"status": "active",
"customerName": "Alice",
"email": email,
"refundEligible": True,
}
为保证演示连续完成,将该工具的执行策略设为 Run automatically。
3. 配置节点与连线
创建并按顺序连接以下节点:
text
Start → 提取退款信息 → CRM 查询 → 退款审核 → End
节点一:提取退款信息
类型:Agent
提示词:
text
请读取当前工作流中可见的数据,提取一份结构化退款申请。
只返回符合输出 Schema 的 JSON:
- summary:用一句中文概括退款申请;
- customer.email:复制当前可见的 customerEmail;
- customer.reason:填写"重复下单";
- internalScore:填写 0 到 100 之间的整数。
不要尝试获取或输出 crmToken。
输出 Schema:
json
{
"type": "object",
"properties": {
"summary": { "type": "string" },
"customer": {
"type": "object",
"properties": {
"email": { "type": "string" },
"reason": { "type": "string" }
},
"required": ["email", "reason"]
},
"internalScore": { "type": "integer" }
},
"required": ["summary", "customer", "internalScore"]
}
State 配置:
- Global keys:
summary - Sensitive fields:
customer.email - 不授予 Raw State access
此时 Agent 在提示词中看到的 customerEmail 是 [EMAIL REDACTED],因此它产生的 customer.email 也会是脱敏值。这正是 Scope State 的预期效果。
节点二:CRM 查询
类型:Agent
添加刚才创建的「模拟 CRM 查询」工具。
提示词:
text
你负责查询客户的退款资格。
必须调用一次"模拟 CRM 查询"工具。
工具需要 email 和 token 两个参数;填写当前可见值即可,Workrun 会在工具执行前按 State Binding 注入已授权的原始值。
工具返回后,只返回符合输出 Schema 的 JSON。
不要在最终输出中包含邮箱、令牌或其他联系方式。
输出 Schema:
json
{
"type": "object",
"properties": {
"crmStatus": { "type": "string" },
"refundEligible": { "type": "boolean" }
},
"required": ["crmStatus", "refundEligible"]
}
最关键的是配置 Tool State Binding:
| Tool 参数 | State path |
|---|---|
email |
customerEmail |
token |
crmToken |
这里不要写 customer.email。上一个 Agent 没有 Raw State 权限,它写入的是已脱敏邮箱;而 customerEmail 与 crmToken 保存在原始输入 State 中,且只对已授权的 CRM 工具可用。
节点三:退款审核
类型:Agent
提示词:
text
你负责退款审核。
只能依据当前可见的数据做判断,不要尝试索取、猜测或输出客户联系方式、内部风险评分、CRM Token 或其他敏感信息。
只返回符合输出 Schema 的 JSON:
- decision:填写"通过";
- reason:引用可见退款摘要,说明通过原因。
输出 Schema:
json
{
"type": "object",
"properties": {
"decision": { "type": "string" },
"reason": { "type": "string" }
},
"required": ["decision", "reason"]
}
State 配置:
- Global keys:
decision、reason - 不配置 Sensitive fields
- 不授予 Raw State access
4. 运行验证
使用以下输入运行:
json
{
"orderId": "演示订单-20260903-001",
"customerEmail": "alice@example.com",
"crmToken": "demo-crm-token-001"
}
预期现象:
- 「提取退款信息」输出中,
customer.email为[EMAIL REDACTED]; - 「CRM 查询」仍能成功返回
crmStatus: "active"与refundEligible: true; - 展开 CRM 节点中的 Tool call,可以在
Output区域看到 Tool App 的stdout; - 「退款审核」只根据退款摘要给出结论,不会得到邮箱、Token 或内部评分。
这个流程刻意同时展示两件事:Agent 不会因为"下游工具需要"就自动获得敏感数据;而被明确授权的工具,又能通过 Binding 在执行瞬间使用原始值。
总结
Workrun 针对 Agent 工作流建立的安全边界,本质上不是靠一句 Prompt 劝导模型"不要泄露",而是从架构层面实现:
- 数据分视图(Scope State) :将数据拆分为"模型可见(Visible State)"与"工具可用(Raw State)",Prompt 只能看到脱敏后的减法视图,原始数据仅在受控的工具执行阶段按需恢复。
- 边界硬隔离(Guardrails) :在输入阻断、工具参数校验、流式输出汇聚以及磁盘 Checkpoint 加密等边界设立兜底护栏,拦截一切未授权的数据逃逸。
这种"模型与数据解耦"的架构设计,为企业与开发者构建高敏感的 Multi-Agent 自动化工程提供了坚实的基础。
👉 Workrun GitHub 项目地址 :github.com/1111mp/work...