1. 从"工具乱调"到"调用有章法":Dogwood 要解决的真问题
如果你最近在折腾 AI Agent,大概率经历过这样的场景:模型明明拿到了工具列表,却把参数拼错、把不该调的工具调了、把两个工具的输出串在一起用,最后给你一个看起来像模像样、实际上完全跑偏的结果。更让人头疼的是,当 Agent 挂上十几个甚至几十个工具之后,调用链路开始变得不可预测——同一个问题,今天跑通,明天就翻车。
Dogwood 这个项目,本质上就是在给"工具调用"这件事立规矩。它不是一个模型,也不是一个 Agent 框架的替代品,而是夹在 Agent 和工具之间的一层调用治理层。你可以把它理解成公司里的"流程审批岗":模型是业务员,工具是各个部门的办事窗口,Dogwood 负责在业务员跑去办事之前,先检查他有没有资格、材料齐不齐、该走哪个窗口、办完之后怎么归档。
关键词里出现的Cedar和MCP是理解 Dogwood 的两把钥匙。Cedar 是一门专门用来写授权策略的语言,它的定位很明确——把"谁能对什么资源做什么操作"这件事,从散落在代码里的 if-else 中抽出来,变成可审计、可测试、可热更新的策略文件。MCP 则是模型和外部工具之间的通信协议,它规定了工具怎么描述自己、怎么被调用、怎么返回结果。Dogwood 做的事情,就是把 Cedar 的策略能力注入到 MCP 的调用流程里,让每一次工具调用都先过一遍策略检查。
这套思路解决的核心痛点有三个。第一是权限失控:Agent 拿到工具列表后,理论上可以调用任何一个,但实际业务里很多工具是有边界的,比如查询接口不该被用来做写操作,内部工具不该被外部会话触发。第二是调用不可审计:出了问题之后,你很难还原"当时模型为什么选了这个工具、传了什么参数、策略有没有拦"。第三是策略硬编码:把权限判断写死在 Agent 代码里,改一条规则就要重新部署,这在多租户、多场景的 Agent 平台里是灾难。
Dogwood 适合谁来参考?如果你正在搭建一个需要接入多个工具、面向多个用户群体、且对调用安全有要求的 AI Agent 平台,那这套思路值得仔细拆。如果你只是做个人玩具项目,一个工具两个工具,那可能用不上这么重的治理层。但只要你开始考虑"这个工具能不能让这个用户调"这个问题,Dogwood 的设计就绕不开了。
2. Dogwood 的架构分层:策略、协议、执行三者怎么咬合
2.1 三层结构:策略层、协议层、执行层各管什么
Dogwood 的架构可以拆成三层来看,每一层的职责边界非常清晰,这也是它比"在 Agent 代码里塞判断"更靠谱的根本原因。
策略层由 Cedar 承担。Cedar 的策略文件长这样:permit(principal, action, resource) when { ... };。principal 是调用方身份,action 是操作类型,resource 是目标资源。Dogwood 把"工具调用"映射成 Cedar 的 action,把"工具本身"映射成 resource,把"发起调用的用户或 Agent 会话"映射成 principal。这样一来,一条策略就能表达"允许用户 A 在会话 B 中调用工具 C,但参数 D 必须满足条件 E"。
协议层由 MCP 承担。MCP 定义了工具的 schema,包括工具名、描述、参数结构、返回结构。Dogwood 在 MCP 的基础上做了一层包装:工具注册进来的时候,除了原始的 MCP schema,还要附带一份 Cedar 策略声明,说明这个工具默认允许谁调、有哪些参数约束。
执行层是 Dogwood 自己的运行时。它接收 Agent 发来的工具调用请求,先解析出 principal、action、resource 三元组,然后交给 Cedar 引擎做策略判定。判定通过,才真正转发给底层工具;判定不通过,直接返回拒绝原因,并且把这次拒绝记录到审计日志里。
这三层的咬合关系可以用一个具体例子说明。假设你有一个"发送邮件"工具,MCP schema 里定义了收件人、主题、正文三个参数。Dogwood 的策略层可以写一条规则:只有 principal 属于"市场部"这个组,且收件人域名是公司内部域名时,才允许调用。执行层收到调用请求后,先提取出 principal(当前会话的用户身份)、action(send_email)、resource(邮件工具),再检查参数里的收件人域名,全部通过才放行。
注意:策略层和执行层的分离是关键。策略文件可以独立于代码热更新,执行层不需要重启就能加载新策略。这一点在多租户场景下尤其重要,因为不同租户的策略可能随时调整。
2.2 为什么选 Cedar 而不是自己写规则引擎
很多人第一反应是:权限判断而已,我自己写个 JSON 配置加几个 if-else 不就行了?为什么要引入 Cedar 这门专门的语言?
我一开始也是这么想的,直到遇到几个具体问题。第一个问题是策略组合。当你有几十条规则,规则之间有优先级、有冲突、有继承关系时,自己写的 if-else 很快就会变成一团乱麻。Cedar 有明确的策略求值语义:默认拒绝,permit 和 forbid 同时命中时 forbid 优先,多条 permit 之间是或的关系。这套语义是形式化定义过的,不会因为代码顺序不同而产生不同结果。
第二个问题是可验证性。Cedar 有配套的分析工具,可以在策略上线前做静态检查,比如"是否存在永远无法命中的策略""是否存在权限泄漏"。自己写的规则引擎要做到这个程度,成本极高。
第三个问题是审计友好。Cedar 的判定过程可以输出完整的决策链路:哪条策略命中了、哪些条件评估为真、哪些为假。这在排查"为什么这次调用被拒了"的时候,比翻代码快得多。
当然,Cedar 也不是没有代价。它的语法需要学习,策略文件的组织需要设计,和现有身份系统的对接需要额外工作。但如果你的 Agent 平台要长期维护、要面对多用户多工具,这个投入是值得的。
2.3 MCP 在 Dogwood 里的角色:不只是工具描述
MCP 在 Dogwood 里不只是"工具怎么描述"的协议,它还承担了调用上下文传递的职责。标准的 MCP 调用里,模型发过来的请求包含工具名和参数。Dogwood 在此基础上扩展了上下文信息:当前会话 ID、用户身份令牌、调用来源(是哪个 Agent 发起的)、时间戳。
这些上下文信息是策略判定的输入。没有它们,Cedar 策略就只能做很粗粒度的判断,比如"这个工具允不允许调用",而做不到"这个用户在此时此场景下允不允许调用"。
MCP 的另一个作用是工具能力的标准化暴露。不同工具的实现方式千差万别,有的走 HTTP,有的走本地函数,有的走数据库。MCP 把它们统一成"输入参数 + 输出结果"的形式,Dogwood 只需要面对这一种形式,策略层和执行层的实现就简化了很多。
3. 工具调用的策略建模:从"能不能调"到"怎么调才合规"
3.1 把工具调用拆成 principal、action、resource 三元组
Cedar 的授权模型是围绕 principal、action、resource 三个维度展开的。要在 Dogwood 里用好 Cedar,第一步就是把"工具调用"这件事准确地映射到这三个维度上。
principal是发起调用的主体。在 Agent 场景里,principal 可能有多层:最终用户、Agent 实例、会话。Dogwood 的做法是把它们组合成一个 principal 层级,比如User::"alice"下面挂Agent::"sales-bot",再下面挂Session::"sess-123"。Cedar 支持 principal 的层级关系,策略里可以写"允许 User::alice 下的任何 Agent 调用某工具",也可以精确到某个会话。
action是操作类型。工具调用天然就是一种 action,但 Dogwood 建议把 action 再细分。比如同样是"查询订单"工具,可以定义QueryOrder和ExportOrder两个 action,前者只读,后者会生成文件。这样策略可以分别控制,避免"能查就能导"的权限放大。
resource是目标资源。工具本身是 resource,但工具操作的数据也是 resource。比如"查询订单"工具,resource 可以是工具本身,也可以是具体的订单 ID。Dogwood 支持在策略里引用请求参数,所以可以写"只允许查询属于当前用户的订单"。
这三者的映射关系需要在工具注册时声明清楚。Dogwood 的做法是要求每个工具在 MCP schema 之外,额外提供一份映射配置,说明这个工具的 action 名称是什么、resource 怎么从参数里提取、principal 从上下文哪里取。
3.2 策略粒度设计:粗粒度、中粒度、细粒度怎么选
策略粒度太粗,等于没做权限控制;太细,维护成本爆炸。Dogwood 的实践里,粒度选择要结合业务场景来定。
粗粒度策略只判断"这个 principal 能不能调用这个工具"。比如"所有登录用户都能调用天气查询工具"。这种策略写起来简单,适合公开的、无副作用的工具。
中粒度策略加入参数约束。比如"用户只能查询自己的订单",策略里会引用请求参数中的用户 ID,和 principal 做比对。这种策略适合大多数业务工具。
细粒度策略会考虑调用频率、时间窗口、数据敏感级别等。比如"同一用户每分钟最多调用 10 次""非工作时间不允许调用财务工具"。这种策略需要执行层提供更多上下文,实现复杂度也更高。
我的建议是:从粗粒度开始,按需细化。不要一上来就设计一套复杂的细粒度策略,因为很多约束在实际运行中可能根本用不上。Dogwood 的策略是热加载的,发现需要加约束时再补,成本很低。
| 粒度 | 判断依据 | 适用场景 | 维护成本 |
|---|---|---|---|
| 粗粒度 | principal + action + resource | 公开工具、无副作用操作 | 低 |
| 中粒度 | 加入参数约束 | 业务工具、数据隔离 | 中 |
| 细粒度 | 加入频率、时间、敏感级别 | 财务、风控、合规场景 | 高 |
3.3 策略冲突与优先级:Cedar 的求值语义怎么用
Cedar 的求值语义是:默认拒绝,permit 和 forbid 同时命中时 forbid 优先。这个语义看起来简单,但在实际策略组织里有很多讲究。
一个常见的坑是策略覆盖不全。比如你写了一条 permit 允许市场部调用邮件工具,又写了一条 forbid 禁止所有人调用外部域名。当市场部用户调用外部域名邮件时,两条策略都命中,forbid 优先,调用被拒。这是符合预期的。但如果你只写了 permit 没写 forbid,那外部域名邮件就会被放行,这可能不是你想要的结果。
另一个坑是策略之间的隐含依赖。比如策略 A 允许调用工具 X,策略 B 允许调用工具 Y,但工具 X 的输出会作为工具 Y 的输入。如果只控制单次调用,不控制调用链,就可能出现"单独看每次调用都合规,但组合起来不合规"的情况。Dogwood 的做法是在执行层维护调用链上下文,策略里可以引用"本次会话已经调用过哪些工具"。
提示:策略文件建议按业务域拆分,比如
email_policies.cedar、order_policies.cedar,每个文件只负责一个域。这样策略冲突更容易定位,也方便不同团队分别维护。
4. 执行层的工程实现:拦截、判定、转发、审计四步走
4.1 调用拦截:在哪里拦、拦什么
Dogwood 的执行层要拦截所有工具调用请求。拦截点的选择很关键:拦得太早,拿不到完整上下文;拦得太晚,可能已经产生了副作用。
Dogwood 的拦截点在 MCP 请求解析之后、工具实际执行之前。这个位置的好处是:请求已经被解析成结构化的工具名和参数,上下文信息也已经从会话中提取出来,但工具还没有真正执行,拦截是安全的。
拦截的内容包括:工具名、参数、principal 信息、会话 ID、调用来源。这些信息会被组装成一个 Cedar 请求,交给策略引擎判定。
有一个细节容易被忽略:参数的类型和格式。MCP 请求里的参数可能是嵌套的 JSON,Cedar 策略里引用参数时需要知道路径。Dogwood 的做法是在工具注册时声明参数的访问路径,比如request.params.recipient.domain,策略里直接用这个路径引用。
4.2 策略判定:Cedar 引擎的调用方式与性能考量
Cedar 引擎的调用本身是同步的,判定一条策略通常在微秒级别。但在高并发场景下,每次工具调用都走一遍策略判定,累积起来也是开销。
Dogwood 做了几层优化。第一层是策略缓存:策略文件加载后编译成内部表示,缓存在内存里,判定时直接使用,不需要每次重新解析。第二层是判定结果缓存:对于相同的 principal、action、resource 组合,如果策略没有变化,判定结果可以缓存。但要注意,如果策略里引用了请求参数,缓存键要包含参数,否则会出错。
第三层是批量判定:当一个 Agent 在一次推理中要调用多个工具时,可以把这些调用请求批量提交给策略引擎,减少往返开销。
性能之外,还有一个正确性问题:策略判定的原子性。如果策略判定通过后,工具执行前,策略被更新了,那这次调用是按旧策略还是新策略?Dogwood 的做法是判定和执行之间加一个短事务,保证判定时使用的策略版本和执行时一致。
4.3 转发与结果处理:调用通过后还要做什么
策略判定通过后,Dogwood 把请求转发给底层工具。转发不是简单的透传,还要做几件事。
参数清洗:策略判定时可能读取了参数,但转发给工具时,有些参数是不应该传下去的,比如内部的会话 ID、用户令牌。Dogwood 在转发前会剥离这些内部字段。
超时控制:工具执行可能很慢,Dogwood 给每个工具配置了超时时间,超时后中断调用并返回错误。这个超时时间应该和策略里的频率限制配合使用,避免慢工具拖垮整个 Agent。
结果包装:工具返回的结果会被包装成 MCP 标准格式,同时附加调用元信息,比如调用耗时、策略判定结果、是否命中缓存。这些元信息对后续审计和优化很有价值。
错误处理:工具执行失败时,Dogwood 要区分是工具本身出错,还是策略判定出错,还是网络问题。不同错误类型返回给 Agent 的信息不同,Agent 的处理策略也不同。
4.4 审计日志:记录什么、怎么存、怎么查
审计日志是 Dogwood 的另一个核心价值。没有审计,策略就是黑盒,出了问题无从查起。
Dogwood 的审计日志记录以下字段:时间戳、principal、action、resource、请求参数摘要、策略判定结果(通过/拒绝)、命中的策略 ID、工具执行结果、耗时。参数摘要不是完整参数,而是脱敏后的关键字段,避免日志里出现敏感数据。
日志的存储要考虑查询需求。如果只是偶尔排查,存文件就够了。但如果要做实时监控、异常告警,就需要存到支持全文检索的系统里。Dogwood 的设计是日志输出到标准接口,具体存哪里由使用方决定。
查询方面,最常见的需求是"某个用户在某段时间内调用了哪些工具""某个工具被拒绝了多少次""哪些策略命中率最高"。这些查询需要日志里有足够的维度信息,所以记录时不要偷懒,该记的字段都要记。
5. 落地时最容易踩的几个坑
5.1 策略写得太细导致维护崩溃
我见过一个团队,一开始就把策略写得非常细,每个工具、每个用户组、每个参数都单独写规则。结果三个月后,策略文件超过两千行,改一条规则要花半天找位置,还经常改出冲突。
Dogwood 的策略设计应该遵循"最小必要"原则。先写粗粒度策略覆盖大部分场景,只对真正有风险的调用写细粒度策略。什么是真正有风险?写操作、涉及资金、涉及敏感数据、跨租户访问。读操作、公开数据、内部工具,粗粒度就够了。
另外,策略要按业务域组织,不要按工具组织。一个业务域下的多个工具,策略往往有共性,放在一起更容易维护。
5.2 上下文信息缺失导致策略失效
策略判定依赖上下文,但上下文信息经常在传递过程中丢失。最常见的是用户身份丢失:Agent 在处理多轮对话时,如果没有把用户身份一路传下来,到了工具调用环节,principal 就变成了"匿名",策略只能按最宽松的规则放行。
Dogwood 的做法是在会话创建时就绑定 principal,并且在整个会话生命周期内保持不变。如果 Agent 需要以不同身份调用工具(比如代用户操作),要显式声明身份切换,并且切换本身也要经过策略判定。
另一个容易丢的是调用来源。同一个工具,可能是用户直接触发,也可能是 Agent 自动触发,还可能是定时任务触发。不同来源的风险级别不同,策略应该能区分。Dogwood 要求调用请求里必须带来源标识,缺失时按最高风险处理。
5.3 策略热更新与执行一致性的冲突
策略热更新是 Dogwood 的卖点,但也带来一致性问题。如果策略在调用过程中被更新,可能出现"判定用旧策略、执行用新策略"的情况。
Dogwood 的解决方案是版本化策略。每次策略更新生成一个新版本,判定时记录使用的版本号,执行时校验版本号是否一致。如果不一致,要么重试判定,要么拒绝调用。这个机制在高频更新策略的场景下会增加一些开销,但保证了正确性。
还有一个实践建议:策略更新要走灰度。不要一次性全量替换,先在小范围生效,观察一段时间再扩大。Dogwood 支持按 principal 或按工具维度灰度,这样出问题时影响面可控。
5.4 性能瓶颈:策略判定成为调用链的短板
前面提到策略判定本身很快,但在高并发下,它可能成为瓶颈。我实测过一个场景:单机每秒 5000 次工具调用,每次调用都走策略判定,CPU 占用率明显上升。
优化手段有几个。缓存判定结果是最直接的,但要注意缓存失效策略。合并判定请求适合批量调用场景。异步判定适合对延迟不敏感的场景,但会引入复杂度。
还有一个容易被忽略的点:策略文件的复杂度。策略里的条件越多、引用的参数越深,判定越慢。如果发现判定成为瓶颈,先检查策略是不是写得太复杂,能不能简化。
6. 从 Dogwood 延伸出去:Agent 工具治理的下一步
Dogwood 解决的是"调用前"的治理问题,但 Agent 工具调用的治理远不止于此。往前往后延伸,还有几个方向值得关注。
调用链治理:单次调用合规,不代表调用链合规。比如 Agent 先调用查询工具拿到数据,再调用发送工具把数据发出去,单独看两次调用都可能合规,但组合起来可能造成数据泄漏。这需要在执行层维护调用链上下文,策略里能引用"本次会话的历史调用"。
动态策略:现在的策略是静态的,写好了就固定。但有些场景需要动态调整,比如检测到异常调用模式时自动收紧策略。这需要策略引擎支持运行时参数注入,以及和监控系统的联动。
跨 Agent 治理:当一个平台上有多个 Agent,它们之间可能互相调用工具,治理复杂度会上升。Dogwood 目前主要面向单 Agent 场景,多 Agent 场景下的 principal 传递、策略继承、审计聚合,都是可以继续深挖的方向。
策略的自动化生成:手写策略容易出错,尤其是策略数量多了之后。能不能从工具的使用日志里学习出策略?比如观察正常调用模式,自动生成基线策略,异常调用则告警。这个方向目前还在探索阶段,但值得关注。
我个人在实际搭建 Agent 平台时的体会是:工具调用治理这件事,早做比晚做好。一开始工具少、用户少,觉得没必要,等工具多了、用户多了再补,改造成本会高很多。Dogwood 这套思路的价值不在于它有多复杂,而在于它把"策略"和"执行"分开了,让治理这件事变得可维护、可演进。哪怕你不直接用 Cedar,把策略从代码里抽出来这个动作本身,就已经解决了一大半问题。
最后分享一个小技巧:策略上线前,先用历史调用日志做一次回放,看看新策略会拒绝哪些调用。如果拒绝的都是预期内的,那可以放心上线;如果拒绝了一些看起来正常的调用,那说明策略写得太严了,需要调整。这个回放机制能帮你避免很多线上事故。