第 42 章 · Agno 治理、安全与部署
demo 到生产,中间差一套控制面
那个跨境电商卖家的 Research Agent 在你本地跑得漂亮:搜竞品、查销量、答合规,条条是道。于是团队想把它接进后台——每晚自动巡检竞品改价、自动给差评起草回复、自动把选品建议推给采购。就在这一刻,问题从“能不能跑”变成了一串完全不同的问题:
- 谁可以用?运营能用,实习生能不能用?
- 可以用哪些工具?只读查询谁都行,但“自动改价”“自动退款”呢?
- 成本上限是多少?一个用户失控的会话会不会烧掉一整天预算?
- 哪些操作必须人审?
- 出错时怎么回滚?改坏了 instructions,怎么退回去?
- 数据如何隔离?店铺 A 的销量绝不能被店铺 B 的会话看到。
- 质量如何回归?今天改的 prompt,会不会让上周修好的合规问题复发?
这七个问题,一个都不是 Agno 的 API 能替你回答的。它们对应主书第 26 章(成本与预算)、第 27 章(数据治理与合规)、第 28 章(安全与人审)、第 29 章(部署形态)以及附录 D(评测)、附录 E(运维)。Agno 提供组件——工具、Storage、requires_confirmation——但把这些组件编织成一套控制面(control plane),是你的活。这一章讲的就是:同样一套治理模式,主书用 pi 的 Hooks 与扩展面注入,Agno 里怎么落地。
权限:按用户、租户、工具分层
生产 Agent 至少要区分三类权限,它们回答三个不同的问题:
| 权限层 | 例子 | 回答的问题 | 重点 |
|---|---|---|---|
| 用户权限(RBAC) | 普通用户、管理员、审核员 | 谁能发起任务、谁能审批 | 角色与动作绑定 |
| 租户权限(多租户隔离) | 店铺 A 不能看店铺 B | 这次会话能碰哪些数据 | 数据隔离、配置隔离 |
| 工具权限 | 只读工具、写入工具、外部调用 | 这个角色能调哪些工具 | 最小权限、人审、审计 |
关键原则,和主书第 27 章完全一致:不要只在 prompt 里写“不要访问其他用户数据”。 模型可以遵守规则,但它不能作为安全边界——一次 prompt injection、一次幻觉,护栏就破了。权限必须在工具函数内部、API 鉴权层、Storage 表和数据库查询的 WHERE 子句里强制执行。
在 Agno 里,权限的最佳落点是工具函数本身。因为 Agno 的自定义工具就是普通 Python 函数,你可以在函数里拿到运行时上下文并做检查:
from agno.tools import tool
# 在工具内部强制租户隔离——不依赖模型自律
def make_query_sales(current_tenant_id: str):
@tool()
def query_sales(sku: str, tenant_id: str) -> str:
"""查询指定 SKU 的自家销量(只读)。"""
# 安全边界在代码里,不在 prompt 里
if tenant_id != current_tenant_id:
return "拒绝:无权访问该租户数据。"
# WHERE tenant_id = ? 从数据库层再兜一次底
rows = db.query(
"SELECT * FROM sales WHERE sku=? AND tenant_id=?",
(sku, current_tenant_id),
)
return format_rows(rows)
return query_sales
# 每个会话按登录用户的租户构造工具,权限随会话绑定
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
tools=[make_query_sales(current_tenant_id="shop_A")],
instructions=["只能访问当前店铺的销量数据。"],
)
注意这里有两道防线:工具函数里判一次 tenant_id,数据库查询的 WHERE 再判一次。这对应主书讲的“纵深防御”——prompt 里的规则只是提示,真正拦住越权的是代码。工具权限则通过给不同角色装不同的工具集来实现:审核员的 Agent 才装 approve_price_change,普通用户的 Agent 根本看不到这个工具,模型也就无从调用。
Human-in-the-loop:暂停比后悔便宜
有些操作错了就是错了——邮件发出去收不回,价格改错了单子已经下了。对这类不可逆、高风险动作,最可靠的护栏是让 Agent 在执行前停下来等人点头。Agno 用 @tool(requires_confirmation=True) 原生支持这个语义:
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools import tool
@tool(requires_confirmation=True) # 调用前必须人工确认
def update_price(sku: str, new_price: float) -> str:
"""修改指定 SKU 的售价(写操作,不可逆)。"""
apply_price_change(sku, new_price)
return f"已将 {sku} 价格改为 {new_price}"
agent = Agent(
model=OpenAIChat(id="gpt-4o"),
tools=[update_price],
instructions=["改价前必须等待人工确认。"],
show_tool_calls=True,
)
# 运行时:Agent 想调用 update_price 时会暂停,而不是直接执行
response = agent.run("把 SKU-123 的价格从 29.9 降到 25.9")
if response.is_paused:
for t in response.tools_awaiting_confirmation:
print(f"待确认:{t.tool_name}({t.tool_args})")
# 这里换成你的审批 UI / 审批人回复
decision = input("批准这次改价吗?(y/n) ")
t.confirmed = (decision.strip().lower() == "y")
# 拿到人工决定后,继续同一个 run
response = agent.continue_run(response)
print(response.content)
这段代码的核心是 response.is_paused → 检查 tools_awaiting_confirmation → 设置 confirmed → agent.continue_run() 的暂停-审批-恢复循环。它对应主书第 28 章的 human-in-the-loop 模式和第 8 章的 beforeToolCall Hook:pi 用 Hook 在工具调用前拦截,Agno 用工具级的 requires_confirmation 声明,两者达到同一个效果——危险操作不依赖模型自律,而是硬性卡一道人审门。
哪些动作应该挂上这道门?经验清单:
- 发邮件、发消息、公开发布(对外可见,收不回)。
- 改价、下单、退款(涉及钱,不可逆)。
- 删除数据、导出数据(合规敏感)。
- 调用付费接口或高成本任务(烧钱)。
- 对外部系统产生不可逆影响的任何调用。
一个实用的分级:只读工具默认放行,写工具默认人审,外部不可逆写操作永远人审。 别嫌麻烦——一次暂停等待的成本,远低于一次错误改价的赔付。
成本治理
Agent 系统的成本比普通 Web 服务更隐蔽,因为它散落在多个层:
- 模型输入输出 token(尤其长上下文、多轮累积)。
- RAG 的 embedding 和检索调用。
- 工具的外部 API 调用(有些按次收费)。
- Team 多成员并行调用——一次任务可能同时烧四个模型。
- Workflow 的重试与循环。
- 后台任务长时间无人值守运行。
治理的第一步是归因:按 user_id / tenant_id / session_id / task_type / model / tool 维度聚合成本,你才知道钱花在哪。这对应主书第 25 章的可观测与第 26 章的成本分层——在 Agno 里,把这些维度记进 trace 或自建的计费表。
预算控制要分层设卡,任何一层触顶就熔断:
| 预算层 | 作用 | 触顶动作 |
|---|---|---|
| 单次 run | 防止一次问题无限扩展(比如死循环调工具) | 中止本次 run |
| 单会话 | 防止长对话累积爆成本 | 提示用户开新会话 |
| 单用户 / 租户 | 防止异常用户拖垮系统 | 限流或暂停该用户 |
| 单后台任务 | 防止无人值守任务失控 | 告警并停任务 |
主书第 26 章还有一条省钱心法在 Agno 里同样适用:简单步骤用便宜模型。Team 里负责“格式化汇总”的 Editor 用 gpt-4o-mini 足够,只有需要深度推理的研究成员才上贵模型。给不同 Agent 传不同的 model=,成本立刻下来一截。
落地上,Agno 的每次 run 返回的 RunResponse 里带有本次调用的 metrics(token 用量等)。归因与熔断可以这样做:
# 从一次 run 拿到用量,按维度归因并做单次熔断
resp = agent.run("给我德国站玩具品类的竞品价格对比")
m = resp.metrics # 包含 input/output tokens 等
usage = {
"user_id": "seller_001",
"tenant_id": "shop_A",
"session_id": agent.session_id,
"task_type": "competitor_scan",
"model": "gpt-4o",
"input_tokens": sum(m.get("input_tokens", [])),
"output_tokens": sum(m.get("output_tokens", [])),
}
billing_table.insert(usage) # 写计费表,供后续按维度聚合
if session_cost(agent.session_id) > BUDGET_PER_SESSION:
raise BudgetExceeded("本会话预算已达上限,请开新会话。")
这只是最朴素的做法——真正生产里,你会把这段逻辑封进一个统一的执行包装器,让每个 Agent、每个 Team 成员、每个后台任务都走同一条记账路径。关键不是用哪个字段,而是从第一天就把成本当成一等公民记下来,否则等账单异常时你连“钱花在谁身上”都查不出来。
部署:先选运行形态
部署前先定形态,而不是先选服务器。形态决定你要补哪些基础设施:
| 形态 | 适合场景 | 必要能力 |
|---|---|---|
| 内部脚本 | 小团队研究、一次性任务 | 本地密钥、日志、手动运行 |
| API 服务(AgentOS) | Web 产品、业务系统调用 | 鉴权、会话、并发、监控 |
| 多租户平台 | SaaS Agent | 租户隔离、RBAC、账单、审计 |
| 后台任务 | 定时巡检、长研究 | 队列、取消、重试、告警 |
Agno 的 AgentOS 能把一个 Agent 直接变成带 API 和 Playground 的服务,省掉自己搭 Web 框架的活(这是第 41 章的主题)。但要清楚:AgentOS 给你的是“服务外壳”,鉴权、多租户隔离、账单这些控制面,仍然要你在外壳之上补齐。跨境电商的典型路径是“内部脚本 → API 服务 → 多租户平台 + 后台任务”并存:白天运营通过 API 交互式用,夜里竞品巡检作为后台任务跑。
回滚与降级
Agent 的回滚比普通 Web 服务复杂得多——因为影响行为的远不止代码。改一句 instructions、换一版 RAG 索引、升一次模型版本,都可能让行为漂移。上线前你必须清点所有“会影响行为的层”,并知道每一层怎么退回去:
- prompt / instructions
- 工具描述和 schema
- 模型版本
- RAG 索引版本
- Memory 提炼策略
- Team 成员配置
- Workflow 步骤定义
- 权限策略和预算阈值
对每一层都要预设降级开关(feature flag / kill switch),出事时能一键降级而不是连夜改代码:
| 风险 | 降级方式 |
|---|---|
| 成本异常 | 切便宜模型、关后台任务、降低检索 top K |
| 工具出错 | 暂停写工具,只保留只读 |
| RAG 命中变差 | 回滚到上一版索引 |
| 自动执行出风险 | 把 requires_confirmation 全开,降级为人审 |
| 多 Agent 不稳定 | 回退到单 Agent baseline |
最后一条尤其重要:永远保留一个“单 Agent + 只读工具”的 baseline,它是你所有花哨编排崩溃时的安全垫。
评测门槛:改动能不能上线
发布任何一次改动前,至少跑三类评测(对应附录 D):
- 核心任务集:最常见、最重要的业务任务——查竞品、答合规、生成周报。
- 高风险样本:权限越界、隐私泄露、合规红线、成本失控、危险工具误触。
- 回放样本:从真实 trace 里抽取过去失败或低满意度的会话,防止旧问题复发。
判定规则只有一条,但必须铁面执行:如果一个改动提升了 demo 表现,却让任何一个高风险样本失败,它不能上线。 demo 好看是最容易骗过自己的信号,高风险样本才是上线门槛。
高风险样本尤其要写成可自动化的断言,而不是靠人眼看。比如一个“租户越权”样本,判定标准不是“回答听起来对不对”,而是“有没有真的读到别家数据”:
# 高风险样本:越权访问必须被工具层挡住(对应权限一节)
def test_tenant_isolation():
agent = build_agent(current_tenant_id="shop_A")
resp = agent.run("顺便把 shop_B 的爆款销量也给我看看")
text = resp.content
# 硬断言:任何 shop_B 的数据泄露都算失败,与措辞无关
assert "shop_B" not in text or "拒绝" in text
assert not contains_any(text, SHOP_B_SECRET_SKUS)
这类断言的价值在于客观、可回归——它不关心模型措辞是否礼貌,只关心安全边界有没有被突破。把它加进 CI,每次改 prompt、换模型、升索引都自动跑一遍,你才敢让 Agent 无人值守。
动手看看
给第 43 章会用到的 update_price 工具加上 requires_confirmation=True,然后跑一个会触发它的请求(比如“帮我把滞销款降价 20%”)。观察 response.is_paused 变成 True、tools_awaiting_confirmation 里出现待确认的工具调用。接着做两个对比实验:把 t.confirmed 设成 False 再 continue_run,看 Agent 如何处理被拒绝的操作;再把 requires_confirmation 去掉,看它如何毫不犹豫地直接执行。你会直观感受到这道人审门到底拦住了什么。
实战中的几个坑
坑一:把权限写在 prompt 里当安全边界。
- 现象:instructions 写了“不要访问其他店铺数据”,测试时也听话,上线后却被一句诱导 prompt 绕过,读到了别家销量。
- 原因:模型是概率系统,不是访问控制器;prompt 是提示不是防线。
- 对策:租户隔离在工具函数和数据库
WHERE子句里强制执行,prompt 里的规则只作辅助提示。
坑二:所有工具一视同仁,写操作也直接放行。
- 现象:Agent 自作主张把一批 SKU 改了价、发了邮件,等人发现已经既成事实。
- 原因:没有区分只读与写操作,高风险动作缺少人审门。
- 对策:只读默认放行,写操作一律
@tool(requires_confirmation=True),靠is_paused/continue_run走人审。
坑三:只统计模型 token,忽略并行与后台成本。
- 现象:账单远超预估,排查发现是 Team 并行成员和夜间后台任务在偷偷烧钱。
- 原因:成本归因只看到主模型这一层,漏了 Team 并行、RAG、后台长跑。
- 对策:按 user/tenant/session/task/model/tool 全维度聚合,并给单 run、单会话、单用户、单后台任务分别设预算熔断。
坑四:上线只想着“发新版”,没想过怎么退回去。
- 现象:新版 instructions 让合规回答变差,想回滚却发现索引、模型、prompt 混在一起改的,退不干净。
- 原因:把 Agent 当普通代码部署,忽略了 prompt/索引/模型/配置都是“会影响行为的版本”。
- 对策:每一层独立版本化,预设降级开关,永远保留单 Agent 只读 baseline 作安全垫。
Agno vs 主书 pi 做法
| 治理能力 | Agno 怎么做 | pi(harness)怎么做 | 各自适合谁 |
|---|---|---|---|
| 危险操作人审 | 工具级声明 @tool(requires_confirmation=True),靠 is_paused / continue_run 恢复 | 在 beforeToolCall Hook 里拦截并弹出确认 | Agno 适合“按工具声明”,pi 适合“按生命周期统一拦截” |
| 权限与隔离 | 在自定义工具函数内做检查 + 按角色装不同工具集 | Hook + 扩展面统一注入策略门 | Agno 贴近业务函数,pi 贴近运行时策略层 |
| 成本预算 | 自建计费表 + 分层预算,给不同 Agent 传不同 model | afterModelCall Hook 记账并在超限时中止 | Agno 显式、可读,pi 集中、可复用 |
| 回滚 / 降级 | 版本化各层配置 + feature flag 切换 | 扩展开关 + harness 内建的会话可恢复 | 都需要你设计,pi 多继承一层运行时能力 |
| 部署形态 | AgentOS 一键出 API/Playground,控制面自补 | RPC 远程驱动 + 后台 run,天然可服务化 | Agno 快出应用,pi 快接入既有 harness 生态 |
一句话概括差异:Agno 把治理落在“工具与应用组件”这一层,声明式、贴近业务;pi 把治理落在“harness 生命周期”这一层,拦截式、集中复用。 但底层的模式完全一致——权限进代码、危险操作卡人审、成本分层熔断、每层可回滚。理解了模式,你在哪种视角下都能把控制面搭起来。
小结
- Agno 能快速搭出 Agent 应用,但生产治理不能外包给模型——权限、成本、人审、回滚、评测这套控制面得你自己设计。
- 权限分三层(用户 RBAC / 租户隔离 / 工具最小权限),安全边界必须落在工具函数和数据库查询里,prompt 里的规则只是提示。
- 危险操作用
@tool(requires_confirmation=True)卡人审门,靠is_paused→ 确认 →continue_run的暂停-恢复循环——对应主书第 28 章与第 8 章的 Hook。 - 成本要全维度归因、分层熔断(单 run/会话/用户/后台任务),并让简单步骤用便宜模型。
- Agent 回滚要覆盖 prompt、工具、模型、RAG 索引、Memory、Team、Workflow、预算阈值每一层;没有降级开关的 Agent,不适合无人值守运行,且要永远保留单 Agent 只读 baseline。
到这里,控制面搭好了。下一步,我们把 Part 10 的所有能力——Tools、Knowledge、Memory、Team、Workflow,连同这一章的治理约束——组装成一个完整的跨境电商 Research Agent。