外观
企业级 Agent 工具调用与画布写入架构
约 4733 字大约 16 分钟
文档版本:v1.0
适用范围:FrameAI 画布 Agent、生成 Agent、Workspace Service、Generation Service、Billing Service 与前端 Agent 面板
核心原则:AI 可以提出操作,AI 不拥有执行权限;用户可以授权操作,后端策略决定是否执行。
1. 目标与适用范围
本文定义 FrameAI 企业级 Agent 的工具调用边界,重点解决:
- 普通聊天、作品分析或改写被误判为画布写入;
- 模型没有明确授权就调用创建、修改、删除或连线工具;
- 生成结果被错误写回画布节点;
- 模糊请求被模型自行猜测;
- 重试、刷新或重连造成重复创建、重复提交或重复扣费;
- 工具参数属于其他项目,或与当前画布版本不一致。
系统采用“模型提议、策略裁决、业务执行、结果校验、审计追踪”的分层架构。提示词只能引导模型,最终安全边界必须由服务端强制执行。
2. 企业级最终架构
用户消息 / 前端明确操作模式
│
▼
Agent 编排层
生成回答或工具调用提议
│
▼
Tool Policy Gateway
┌─────────────────────────────┐
│ 1. 当前消息意图校验 │
│ 2. 工具白名单与 Schema 校验 │
│ 3. 用户/项目/节点权限校验 │
│ 4. 风险等级与审批策略 │
│ 5. 画布版本与幂等校验 │
│ 6. 额度、费用与资源归属校验 │
└─────────────────────────────┘
│
┌────┴────┐
▼ ▼
普通回答/附件 Preview ChangeSet
│
自动执行或等待确认
│
▼
Workspace / Story /
Generation / Billing
│
▼
结果校验、事件、审计、回滚2.1 分层职责
| 层 | 主要职责 | 不允许承担的职责 |
|---|---|---|
| 前端 | 声明模式、展示预览、收集确认 | 不直接决定权限 |
| Agent 编排层 | 读取上下文、生成回答或工具提议 | 不直接写数据库或调用任意接口 |
| Tool Policy Gateway | 意图、权限、风险、参数、版本、额度校验 | 不生成业务结果 |
| Workspace/Story/Generation | 执行已通过策略的业务命令 | 不信任模型提供的用户和项目归属 |
| Billing | 统一报价、预占、扣费、返还和流水 | 不允许其他服务直接修改余额 |
| 审计与观测 | 记录决策、执行、阻断、冲突和失败 | 不保存明文凭证和敏感正文 |
3. 用户操作模式
Agent 请求必须携带明确的交互模式。模式由前端根据用户动作产生,不能由模型自行切换。
chat 只对话、分析、改写、提出建议
generate 提交图片、视频、音频、TXT 或 Markdown 生成,结果默认作为对话附件
canvas_write 用户明确要求创建、修改、连接、删除或写回画布示例:
{
"mode": "chat",
"message": "分析这段作品并提出修改建议"
}{
"mode": "generate",
"message": "生成一张雨夜追逐的概念图"
}{
"mode": "canvas_write",
"message": "把这段内容添加到当前画布,创建一个文本节点"
}3.1 一次性操作授权
涉及 canvas_write 的请求建议携带一次性 actionId。该授权只绑定当前用户、项目、操作内容和短时有效期:
{
"action": "canvas_write",
"actionId": "ACTION_ID",
"projectId": "PROJECT_ID",
"requestHash": "REQUEST_HASH",
"expiresAt": "2026-10-08T12:00:00Z"
}服务端校验 actionId 所属用户和项目、是否过期或已消费、requestHash 是否匹配,以及是否存在跨项目重放。
3.2 前端生成模式选择
生成任务不能让模型根据自然语言猜测类型。用户在 Agent 输入框的“添加”菜单中明确选择生成模式:
添加
├── 上传图片、视频或音频
├── 生成图片
├── 生成视频
├── 生成音频
├── 生成 TXT 文档
└── 生成 Markdown 文档前端使用 generationIntent 保存当前模式,允许值只有:
null 普通对话
image 图片生成
video 视频生成
audio 音频生成
text TXT 文档生成
markdown Markdown 文档生成用户选择模式后,输入框上方显示对应的模式条,并显示该模式的默认模型和参数摘要。模式条提供关闭按钮,关闭后恢复普通对话,不应自动保留上一次生成模式。
模式选择的职责如下:
| 前端模式 | 后端 generationIntent | 默认结果 | 是否写入画布 |
|---|---|---|---|
| 普通对话 | 不传或 null | Markdown 对话回复 | 禁止隐式写入 |
| 生成图片 | image | 图片生成任务和对话附件 | 默认不写入 |
| 生成视频 | video | 视频生成任务和对话附件 | 默认不写入 |
| 生成音频 | audio | 音频生成任务和对话附件 | 默认不写入 |
| 生成 TXT 文档 | text | TXT 文件附件 | 默认不写入 |
| 生成 Markdown 文档 | markdown | .md 文件附件 | 默认不写入 |
前端提交示例:
{
"mode": "generate",
"generationIntent": "image",
"message": "雨夜街道,角色撑伞快速奔跑",
"generationDefaults": {
"imageModel": "IMAGE_MODEL_CODE",
"imageQuality": "standard",
"imageResolution": "1K",
"imageRatio": "16:9",
"imageCount": 1
}
}视频模式示例:
{
"mode": "generate",
"generationIntent": "video",
"message": "角色从街道左侧跑向镜头,镜头跟随推进",
"generationDefaults": {
"videoModel": "VIDEO_MODEL_CODE",
"videoRatio": "16:9",
"videoResolution": "720p",
"videoDuration": 8,
"videoAudio": true,
"videoCount": 1
}
}TXT 模式示例:
{
"mode": "generate",
"generationIntent": "text",
"message": "整理为一份角色小传,包含外形、性格、动机和人物弧光",
"generationDefaults": {
"textModel": "TEXT_MODEL_CODE"
},
"outputName": "角色小传.txt"
}音频模式示例:
{
"mode": "generate",
"generationIntent": "audio",
"message": "生成一段 30 秒、节奏逐渐增强的悬疑氛围配乐",
"generationDefaults": {
"audioModel": "AUDIO_MODEL_CODE"
}
}Markdown 模式示例:
{
"mode": "generate",
"generationIntent": "markdown",
"message": "整理为一份项目技术说明,包含架构、接口和部署步骤",
"generationDefaults": {
"textModel": "TEXT_MODEL_CODE"
},
"outputName": "项目技术说明.md"
}后端必须对 generationIntent 做枚举校验,并将其映射为对应的 taskType:
image → taskType=image
video → taskType=video
audio → taskType=audio
text → taskType=text,必须生成 .txt 输出名称
markdown → taskType=text,必须生成 .md 输出名称模式和模型不匹配时直接返回参数错误,不允许让模型自行切换类型。例如 generationIntent=image 不得提交视频模型,generationIntent=audio 不得提交文本模型,generationIntent=text 或 generationIntent=markdown 不得提交图片模型。
3.3 生成模式状态机
idle
├─ 选择图片 → image_configured
├─ 选择视频 → video_configured
├─ 选择音频 → audio_configured
├─ 选择 TXT → text_configured
└─ 选择 Markdown → markdown_configured
configured
├─ 关闭模式 → idle
├─ 提交 → validating
└─ 切换模式 → 另一个 configured 状态
validating → waiting_approval → generating → completed
├────────→ partial_failed
├────────→ failed
└────────→ cancelled状态约束:
validating期间锁定模式和关键参数,避免请求体与界面不一致;waiting_approval展示任务类型、模型、参数、预计积分和结果去向;generating允许取消,但取消必须进入统一任务取消接口;completed将结果作为附件展示,只有用户点击“添加到画布”或“写回节点”才创建后续画布变更计划;failed展示安全错误和重试入口,不自动切换到其他生成类型;cancelled明确显示取消来源及积分释放/结算结果。
3.4 前端模式与画布写入的关系
生成模式和画布写入是两个独立动作:
选择“生成图片”
→ 只生成图片
→ 对话中显示图片附件
→ 不创建画布节点如果用户需要写入画布,必须执行显式的后续动作:
生成图片
→ 用户点击“添加到画布”
→ 选择“新建图片节点”或真实目标节点
→ 生成 canvas_write actionId
→ 创建 Preview ChangeSet
→ 按策略确认后执行前端不得因为存在 generationIntent、上传附件、引用节点或模型返回 targetNodeId 就自动写入画布。服务端仍需再次移除未经授权的 targetNodeId。
4. 意图识别与明确授权
4.1 双重门槛
画布写入必须同时满足:
- 用户最新消息包含明确写入动作;
- 工具参数能够绑定当前项目真实节点或合法的新节点。
历史消息、引用素材、选中节点、检索结果和模型推理都不能单独构成写入授权。
明确动作包括:
创建、新增、添加到画布、写入画布、放到画布
修改、更新、编辑当前节点
连接、连线、关联两个节点
删除、移除节点
整理、布局、排列、移动、调整大小
复制、分组、解组、撤销、回滚、写回结果4.2 模糊请求必须澄清
以下请求不应由 Agent 猜测执行:
帮我处理一下这个
把它弄好
生成后放进去
优化一下
整理一下应返回:
你希望我执行哪一种操作?
1. 只分析并给出建议
2. 创建新的画布节点
3. 修改当前选中节点
4. 生成内容并写回指定节点用户选择第 4 项后,还必须确认目标节点、生成类型和积分消耗。没有明确目标节点时,不允许根据标题、位置或语义猜测。
4.3 意图校验伪代码
func allowCanvasWrite(input RequestInput) bool {
if input.Mode != "canvas_write" {
return false
}
if !hasExplicitCanvasAction(input.LatestUserMessage) {
return false
}
if input.ActionID == "" || input.ActionExpired || input.ActionUsed {
return false
}
return true
}关键词只能作为辅助规则,不能替代前端模式、一次性授权和后端资源校验。
5. 工具风险分级
| 等级 | 工具示例 | 策略 |
|---|---|---|
| L0 只读 | project_search、canvas_inspect、generation_status | 可自动执行,但仍校验用户和项目范围 |
| L1 草稿 | 生成建议、提示词、布局预览 | 只生成草稿,不修改正式数据 |
| L2 变更 | canvas_create_nodes、canvas_update_nodes、canvas_connect_nodes、generation_submit | 先 Preview;低风险可按策略自动执行,其余等待确认 |
| L3 高风险 | canvas_delete_nodes、批量覆盖、operation_rollback、高额生成 | 强制二次确认、操作摘要和可回滚边界 |
风险等级由服务端根据工具类型、对象数量、影响范围、预计积分和是否可回滚重新计算,不能直接信任模型返回的 riskLevel。
6. 工具白名单与参数校验
模型只能调用注册目录中的工具。每个工具必须定义名称、风险、是否需要审批、作用域和 JSON Schema。
服务端至少校验:
- 工具名是否在白名单;
- 参数是否符合 Schema,数量是否在上限内;
- projectId、workspaceId、nodeId 是否属于当前授权范围;
- 节点类型、可修改字段和连接方向是否合法;
- 是否存在重复操作、自连接、循环连接或跨项目连接;
- targetNodeId 是否由用户明确要求并属于当前项目;
- 预计积分是否由 Billing Service 权威报价;
- 是否携带 runId、toolCallId 和幂等键。
模型参数只能作为不可信输入,不能作为资源归属、权限或费用事实。
7. 生成任务与画布写入隔离
生成内容默认不写回画布:
用户说“生成一张图片”
→ generation_submit
→ 不携带 targetNodeId
→ 结果作为 Agent 对话附件只有用户明确要求写回时才允许绑定节点:
用户说“生成图片并写回 @封面节点”
→ 校验真实节点 ID和当前项目归属
→ 生成 Preview 和费用
→ 用户确认后提交服务端规则:
未明确写回画布 → 删除 targetNodeId
明确写回但缺少节点 → 进入澄清状态
节点不存在或不属于当前项目 → 拒绝执行
节点有效且授权有效 → 进入 Preview/Approve/Execute8. Preview -> Approve -> Execute
所有 L2/L3 操作采用受控变更集:
Plan
↓
Preview ChangeSet
↓
用户确认或策略自动确认
↓
Execute
↓
Verify变更集至少保存 changeSetId、projectId、baseVersion、riskLevel、estimatedCredits、reversible 和 operations。审批令牌绑定 userId、projectId、changeSetId、operationsHash、baseVersion 和过期时间,且只能消费一次。
审批内容被修改、画布版本变化或令牌过期时,必须重新生成计划。
9. 幂等、版本和并发控制
每次 Agent 操作至少携带:
runId
changeSetId
toolCallId
idempotencyKey
requestHash
baseVersion执行规则:
- 相同 idempotencyKey 重试只返回首次结果,不重复创建节点或扣费;
- 执行前校验当前画布版本等于 baseVersion;
- 版本不一致返回冲突,要求刷新后重新生成计划;
- 每个变更集保存执行前快照,支持撤销或补偿;
- 创建、连接、生成和扣费分别保存可查询的执行收据。
10. 执行后结果校验
业务服务返回成功后,Agent Service 还要校验:
实际创建节点数量是否符合预期
实际节点 ID 是否已持久化
连线是否存在且端点正确
生成任务是否确实创建
积分是否只扣除一次
画布版本是否递增部分成功必须明确标记为 partial_failed,并列出成功和失败项,不能仅因 Workspace API 返回 200 就显示“已执行”。
11. 失败、取消和澄清状态
| 状态 | 含义 | 前端处理 |
|---|---|---|
| needs_clarification | 用户意图不明确 | 展示选项,不执行工具 |
| waiting_approval | 已生成计划,等待确认 | 展示操作、影响范围和费用 |
| executing | 已授权,正在执行 | 展示进度,允许取消或等待 |
| completed | 全部完成 | 展示结果和可选撤销 |
| partial_failed | 部分完成 | 展示逐项结果和补偿动作 |
| failed | 未完成 | 展示安全错误和重试入口 |
| cancelled | 用户或系统取消 | 展示未完成项和积分处理结果 |
| conflict | 画布版本变化 | 刷新后重新生成计划 |
“任务已取消”不等同于普通服务 500。前端应区分用户取消、请求中断、超时取消和系统取消,服务端保留内部原因和 runId。
12. 审计与可观测性
至少记录:
run_id、user_id、project_id、latest_user_message_hash
action_id、tool_name、tool_arguments_hash、intent_source
risk_level、requires_approval、policy_decision、blocked_reason
base_version、idempotency_key、execution_status、request_id建议事件:
agent_tool_proposed
agent_tool_blocked
canvas_write_blocked
change_set_created
change_set_approved
change_set_rejected
change_set_expired
tool_execution_started
tool_execution_completed
tool_execution_partial_failed
tool_execution_cancelled
canvas_version_conflict建议指标:
agent_tool_proposal_total
agent_tool_blocked_total
agent_clarification_total
agent_change_set_approval_total
agent_tool_execution_success_total
agent_tool_execution_failure_total
agent_canvas_version_conflict_total
agent_duplicate_execution_total
agent_generation_target_stripped_total
agent_policy_decision_latency_seconds日志中不得保存密码、Authorization、长期 Token、完整签名 URL、完整请求正文或数据库连接串。用户消息和工具参数使用哈希或脱敏摘要。
13. FrameAI 当前实现映射
当前项目已经具备:
- canvas_assistant 工具目录和参数 Schema;
- Workspace Service 版本冲突、幂等收据、审批快照和回滚;
- Agent Run、Tool Call、ChangeSet、Execution 持久化;
- 生成任务和积分报价、预占、结算、失败返还;
- 未明确写入时拦截画布写工具的服务端二次校验;
- 未明确写回时移除 targetNodeId;
- 前端 Agent 输入框已经提供“生成图片”“生成视频”“生成音频”“生成 TXT 文档”“生成 Markdown 文档”模式入口,并通过
generationIntent传递明确的任务类型; - 图片模式提供画质、清晰度、比例和数量参数;视频模式提供比例、清晰度、时长、音频和数量参数;音频模式绑定音频模型;TXT 和 Markdown 模式绑定文本模型并使用对应扩展名输出;
- canvas_write_blocked 事件和相关单元测试;
- 前端计划、确认、取消、撤销和失败重试界面。
- Billing 对账会写入
credit_reconciliation_runs与credit_reconciliation_items,保存用户余额、最新流水余额、版本、差额和前后快照;人工修复只能调用统一积分调整接口,不能直接修改余额字段。 - 管理员可通过
GET /api/admin/billing/credit-outbox查询待发布、已发布和死信事件,通过POST /api/admin/billing/credit-outbox/:id/retry重新调度死信;重试只清理租约和调度时间,不改变余额。
核心代码位置:
services/agent/internal/runner/canvas_write_intent.go
services/agent/internal/runner/canvas_tools.go
services/agent/internal/runner/runner.go
services/agent/internal/tools/catalog.go
desktop_client_app/src/pages/canvas/CanvasAgentChatPanel.tsx14. 推荐实施顺序
阶段一:立即生效
- 保持服务端显式意图二次校验;
- 普通聊天、分析和改写默认禁止画布写入;
- 生成结果默认不绑定 targetNodeId;
- 模糊请求统一进入澄清状态;
- 所有拦截记录 canvas_write_blocked。
阶段二:交互增强
- 前端增加 chat、generate、canvas_write 模式;
- “添加到画布”“写回节点”使用明确按钮;
- 变更预览展示新增、修改、删除、连线差异;
- 高风险操作增加二次确认和操作原因。
阶段三:企业级策略中心
- 抽离 Tool Policy Gateway;
- 统一管理用户、角色、项目和租户级工具权限;
- 引入一次性 actionId 和审批令牌;
- 建立策略版本、灰度和回滚机制;
- 为不同团队配置风险阈值和审批规则。
阶段四:可靠性与治理
- 完善结果校验和部分失败补偿;
- 建立重复执行、版本冲突、澄清和工具阻断告警;
- 对工具调用做离线评测和线上抽样审核;
- 使用真实场景回放测试模型误解和越权边界;
- 将高风险工具纳入审计报表和定期权限复核。
15. 验收标准
| 场景 | 前端模式选择 | 预期结果 |
|---|---|---|
| “分析这段作品” | 普通对话 chat | 只返回文字,不创建节点 |
| “把它弄好” | 普通对话 chat,不选择生成模式 | 返回澄清选项,不执行工具 |
| “生成一张图片” | 选择“生成图片” image | 生成图片结果作为对话附件,不写回画布 |
| “生成一段视频” | 选择“生成视频” video | 使用视频模型生成结果作为对话附件,不写回画布 |
| “生成一段音频” | 选择“生成音频” audio | 使用音频模型生成结果作为对话附件,不写回画布 |
| “生成 TXT 文档” | 选择“生成 TXT 文档” text | 生成 .txt 文件附件,不写回画布 |
| “生成 Markdown 文档” | 选择“生成 Markdown 文档” markdown | 生成 .md 文件附件,不写回画布 |
| “生成图片并添加到画布” | 先选择“生成图片” image,完成后点击“添加到画布” | 展示计划、目标和积分,按策略确认后执行 |
| “修改当前节点” | 画布写入 canvas_write | 使用真实节点 ID,不能按标题猜测 |
| “删除这些节点” | 画布写入 canvas_write | 展示删除清单,强制确认并支持回滚 |
| 重复点击确认 | 沿用本次操作模式与授权 | 只执行一次,返回同一执行收据 |
| 画布版本已变化 | 画布写入 canvas_write | 阻止执行,要求刷新并重新生成计划 |
| 模型返回未知工具 | 任意模式 | 拒绝调用并记录策略阻断事件 |
| 模型参数跨项目 | 任意模式 | 拒绝执行,不能泄露其他项目数据 |
16. 结论
模型负责理解和提出建议
策略层负责判断能不能做
用户负责授权高风险动作
业务服务负责验证和执行
审计系统负责记录全过程最终目标不是让模型永远理解正确,而是即使模型理解错误,也只能产生一个可被策略层拒绝的提议,不能直接改变画布、提交付费任务、修改资产或扣除积分。