从零打造 AI Agent:系统加固篇(S07-S11)
🔒 从零打造 AI Agent:系统加固篇(S07-S11)
基础打好了,现在要给它装上安全带、加上扩展槽、装上记忆芯片。
🎬 开场:你的 AI Agent 安全吗?
想象你造了一个机器人,帮你在公司里跑腿:
- 它可以帮你发邮件 ✅
- 它可以帮你删文件 ❌
- 它可以帮你在服务器上执行命令 ❌❌❌
如果没有任何限制,机器人可能会:
- 把重要文件删了
- 执行危险的 shell 命令
- 访问不应该访问的数据
更可怕的是:如果它遇到一点小问题就"死机",你得从头来…
这就是为什么我们需要系统加固。
📚 上集回顾:核心闭环
上篇文章我们讲了打造 Agent 的基础:
| 章节 | 核心问题 | 解决方案 |
|---|---|---|
| S01 Agent Loop | 如何让 AI 从"会说话"变"会干活" | 把现实结果喂回模型 |
| S02 工具使用 | 如何扩展 Agent 的能力 | dispatch map 路由表 |
| S03 待办写入 | 如何让 Agent 记住"做到哪一步了" | pending → in_progress → completed |
| S04 子代理 | 如何隔离上下文,防止干扰 | 新上下文 + 摘要返回 |
| S05 技能系统 | 如何按需加载专业知识 | 轻量目录 + 按需加载 |
| S06 上下文压缩 | 如何保持上下文精简 | 大结果落盘 + 旧结果缩短 + 整体摘要 |
今天,我们给这个基础加上安全、稳定、可扩展的能力。
🛡️ S07:权限系统 - 意图先过安全闸门
🎬 精彩开场:保安的故事
想象你住在一个高档小区:
没有保安的情况:
- 任何人想进就能进
- 送外卖的可以进
- 贴小广告的可以进
- 小偷也可以进(太危险了!)
有保安的情况:
- 住户 → 直接放行
- 访客 → 打电话确认
- 可疑人员 → 拒绝进入
权限系统就是 Agent 的"保安"——工具调用不能直接执行,必须先过一道安全检查。
核心问题
模型可以提出行动建议,但如果直接执行,可能会:
- 删掉重要文件
- 执行危险命令
- 在不该动手的时候动手
解决方案
工具意图不能直接变成执行,中间必须经过权限检查。
四步检查流程
tool_call(工具意图)
↓
1. deny rules(优先检查)
↓ 命中 → deny(拒绝)
2. mode check(根据当前模式)
↓
3. allow rules(白名单)
↓ 命中 → allow(放行)
4. ask user(都没命中)
↓
最终决策:allow / ask / deny
三种权限模式
| 模式 | 含义 | 适合场景 |
|---|---|---|
default |
未命中规则时问用户 | 日常交互 |
plan |
只允许读,不允许写 | 计划、审查、分析 |
auto |
简单安全操作自动过 | 高流畅度探索 |
核心代码
def check_permission(tool_name: str, tool_input: dict) -> dict:
# 1. deny rules(优先)
for rule in deny_rules:
if matches(rule, tool_name, tool_input):
return {"behavior": "deny", "reason": "危险操作"}
# 2. mode check
if mode == "plan" and tool_name in WRITE_TOOLS:
return {"behavior": "deny", "reason": "plan 模式禁止写操作"}
# 3. allow rules
for rule in allow_rules:
if matches(rule, tool_name, tool_input):
return {"behavior": "allow", "reason": "白名单"}
# 4. ask
return {"behavior": "ask", "reason": "需要确认"}
为什么顺序是 deny → mode → allow → ask
- deny 先于 allow:有些东西不应该交给"模式"决定,要优先挡掉
- mode 在中间:模式决定当前会话的大方向
- allow 在 deny 之后:安全、重复、常见的操作可以直接过
- ask 在最后:前面都没命中的灰区,才交给用户
Bash 为什么值得单独讲
bash 是"可执行动作描述",必须额外检查危险模式:
sudorm -rfcurl | sh- Fork bomb
新手最容易踩的4个坑
- deny 和 allow 顺序搞反 → 必须 deny 先于 allow
- 不把 bash 当成危险品 → bash 是最危险的工具
- 权限系统只是一个开关 → 它是一条管道,不是简单的允许/禁止
- 一上来就做很复杂的模式系统 → 先做 3 个模式(default/plan/auto)
🔌 S08:Hook 系统 - 不改主循环也能扩展
🎬 精彩开场:插座的故事
想象你要装修新房:
没有插座的墙:
- 你想在墙上挂电视 → 没法插电
- 你想在沙发旁边放灯 → 没地方插
- 每次想加一个新电器,都要重新拉线
有插座的墙:
- 电线已经埋好了
- 墙上留了插座
- 以后想加什么电器,直接插上去就行
Hook 就是软件的"插座"——主循环提前留好了接口,未来想扩展功能,直接"插上去"就行,不用改主循环。
核心问题
每次想扩展功能都要改主循环,主循环越来越重,最后谁都不敢动。
解决方案
主循环只负责暴露"时机",真正的附加行为交给 Hook。
三种事件 + 返回码
| 事件 | 时机 | 返回码 |
|---|---|---|
SessionStart |
会话开始 | 0=继续 |
PreToolUse |
工具执行前 | 0=继续 / 1=阻止 / 2=注入消息 |
PostToolUse |
工具执行后 | 0=继续 / 2=注入消息 |
工作流程
主循环继续往前跑
|
+-- 到了某个预留时机
|
+-- 调用 hook runner
|
+-- 收到 hook 返回结果
|
+-- 决定继续、阻止、还是补充说明
核心代码
# 注册 Hook
HOOKS = {
"SessionStart": [on_session_start],
"PreToolUse": [pre_tool_guard],
"PostToolUse": [post_tool_log],
}
# 执行 Hook
def run_hooks(event_name: str, payload: dict) -> dict:
for handler in HOOKS.get(event_name, []):
result = handler(payload)
if result["exit_code"] in (1, 2):
return result
return {"exit_code": 0, "message": ""}
统一返回约定
| 退出码 | 含义 |
|---|---|
0 |
正常继续 |
1 |
阻止当前动作 |
2 |
注入一条补充消息,再继续 |
新手最容易踩的4个坑
- 把 hook 当成"到处插 if" → 它是预留插口,不是条件分支
- 没有统一的返回结构 → 今天返回字符串,明天返回布尔值
- 一上来就把所有事件做全 → 先学会 3 个事件,再扩展
- 忘了说明"教学版统一语义"和"高完成度细化语义"的区别 → 先学统一模型,再学事件细化
🧠 S09:记忆系统 - 只保存跨会话还成立的东西
🎬 精彩开场:鱼的记忆?
我们都听说过"鱼的记忆只有 7 秒"。但想象一下,如果 AI 也是这样呢?
第一天
你:“我姓李,叫李明”
AI:“好的,李明先生”
第二天
你:“帮我写代码”
AI:“好的!”
你:“你知道我叫什么吗?”
AI:“不知道”
第三天
你:“我最近在学 Python”
AI:“好的!”
你:“我之前告诉过你我的名字是什么吗?”
AI:“不知道”
这样的 AI,你崩溃不崩溃?
核心问题
AI 每次新会话都完全从零开始:
- 用户姓什么?不记得
- 之前纠正过什么?不记得
- 项目有什么约定?不记得
解决方案
只有跨会话仍然有价值、无法从当前工作重新推导的信息,才值得进入 memory。
四类 Memory
| 类型 | 解释 | 示例 |
|---|---|---|
user |
用户偏好 | 喜欢 tabs 缩进、回答希望简洁 |
feedback |
用户纠正过的地方 | “不要这样改”、“以后遇到这种情况先做 X” |
project |
不容易从代码推出的项目约定 | 设计决定是因为合规、某个目录短期不能动 |
reference |
外部资源指针 | 某个问题在哪个看板、监控面板在哪 |
什么不要存进 Memory
| 不要存 | 为什么 |
|---|---|
| 文件结构、函数签名 | 可以重新读代码得到 |
| 当前任务进度 | 属于 task/plan,不属于 memory |
| 临时分支名、当前 PR 号 | 很快会过时 |
| 修 bug 的具体代码细节 | 代码和提交记录才是准确信息 |
| 密钥、密码、凭证 | 安全风险 |
Memory、Task、Plan、CLAUDE.md 的边界
| 概念 | 定义 | 判断方法 |
|---|---|---|
| memory | 跨会话仍有价值的信息 | 以后很多会话可能还有用?→ memory |
| task | 当前工作要做什么 | 只对这次任务有用?→ task |
| plan | 这一轮我要怎么做 | 只对这次任务有用?→ plan |
| CLAUDE.md | 更稳定的规则说明 | 属于长期系统级固定说明?→ CLAUDE.md |
新手最容易踩的5个坑
- 把代码结构也存进 memory → 系统完全可以重新读,不需要记
- 把当前任务状态存进 memory → 这属于 task/plan,不是 memory
- 把 memory 当成绝对真相 → memory 可能过时,要优先相信当前观察
- 用户说"忽略 memory"时还继续用 → 当 memory 是空的
- 推荐具体路径时不再验证 → memory 只是方向,要验证后再结论
🔧 S10:系统提示词 - 把输入组装成流水线
🎬 精彩开场:组装电脑
想象你要组装一台电脑:
没有流水线的情况:
- 你买了一个"万能电脑"
- 里面什么配件都有:CPU、显卡、内存、硬盘、显示器、键盘、鼠标…
- 全部焊死在主板上,不能换
- 想升级内存?换电脑
有流水线的情况:
- 你买了一个"可组装的电脑"
- 主板上有插槽:CPU 插槽、内存插槽、硬盘插槽…
- 想升级内存?拔掉旧的,插上新的
- 灵活、方便、可扩展
System Prompt 流水线就是这种思想:把不同来源的信息分开管理,按需组装。
核心问题
system prompt 写成一坨固定文本,越来越臃肿,维护困难。
解决方案
system prompt 不是一坨固定文本,而是一条按阶段拼装的流水线。
六段组装结构
1. core(核心身份)
2. tools(工具列表)
3. skills(skills 元信息)
4. memory(记忆内容)
5. claude_md(指令链)
6. dynamic_context(动态信息)
然后按顺序拼起来:
core
+ tools
+ skills
+ memory
+ claude_md
+ dynamic_context
= final system prompt
为什么不能把所有东西都塞进一个大字符串
- 不好维护:不知道哪一段来自哪里,该修改哪一部分
- 不好测试:很难分别测试每个部分
- 不好做缓存和动态更新:稳定内容和临时内容混在一起
System Prompt vs System Reminder
| 类型 | 适合放什么 |
|---|---|
| System Prompt | 身份、规则、工具、长期约束 |
| System Reminder | 这一轮才临时需要的补充上下文、当前变动的状态 |
核心代码
class SystemPromptBuilder:
def build(self) -> str:
parts = []
parts.append(self._build_core()) # 核心身份
parts.append(self._build_tools()) # 工具列表
parts.append(self._build_skills()) # skills 元信息
parts.append(self._build_memory()) # memory 内容
parts.append(self._build_claude_md()) # CLAUDE.md 指令链
parts.append(self._build_dynamic()) # 动态信息
return "\n\n".join(p for p in parts if p)
⚡ S11:错误恢复 - 先恢复,再继续
🎬 精彩开场:司机与路况
想象你是一个司机,开车去一个陌生的地方:
没有错误恢复的情况:
- 遇到一个红灯 → 停车,不知道接下来怎么办 → 卡住了
- 遇到修路封路 → 停车,不知道绕哪条路 → 卡住了
- 遇到导航没信号 → 停车,不知道往哪走 → 卡住了
有错误恢复的情况:
- 遇到红灯 → 等一等,绿灯亮了继续开
- 遇到修路封路 → 导航重新规划路线,换条路走
- 遇到导航没信号 → 缓存上次路线,继续开
错误恢复就是让 AI"遇到问题不卡住",知道该怎么继续。
核心问题
遇到错误就"死机",任务中断,只能从头来。
解决方案
错误先分类,恢复再执行,失败最后才暴露给用户。
三类问题 + 三条恢复路径
| 问题类型 | 恢复路径 |
|---|---|
| 输出被截断 | 注入续写提示,再试一次 |
| 上下文太长 | 压缩旧上下文,再试一次 |
| 临时连接失败 | 等一会儿,再试一次 |
工作流程
LLM call
|
+-- stop_reason == "max_tokens"
| -> 注入续写提示
| -> 再试一次
|
+-- prompt too long
| -> 压缩旧上下文
| -> 再试一次
|
+-- timeout / rate limit / transient error
-> 等一会儿
-> 再试一次
续写提示为什么重要
# 错误写法
CONTINUE_MESSAGE = "continue"
# 正确写法
CONTINUE_MESSAGE = (
"Output limit hit. Continue directly from where you stopped. "
"Do not restart or repeat."
)
核心代码
def choose_recovery(stop_reason: str | None, error_text: str | None) -> dict:
if stop_reason == "max_tokens":
return {"kind": "continue", "reason": "输出截断,注入续写"}
if error_text and "prompt" in error_text and "long" in error_text:
return {"kind": "compact", "reason": "上下文过长,压缩"}
if error_text and any(word in error_text for word in ["timeout", "rate", "connection"]):
return {"kind": "backoff", "reason": "连接失败,等待重试"}
return {"kind": "fail", "reason": "不可恢复的错误"}
新手最容易踩的5个坑
- 把所有错误都当成一种 → 该续写的去压缩,该等待的去重试
- 没有重试预算 → 可能无限循环
- 续写提示写得太模糊 → 模型可能重新总结
- 压缩后不告诉模型"这是续场" → 模型可能重新向用户提问
- 恢复过程完全没有日志 → 看不清主循环做了什么
🎯 核心公式总结
系统加固 = 权限系统 + Hook 系统 + 记忆系统 + 提示词流水线 + 错误恢复
权限 = deny → mode → allow → ask
Hook = 事件 + payload + 返回码(0/1/2)
Memory = user / feedback / project / reference
Prompt = core + tools + skills + memory + claude_md + dynamic
Error = 分类 → 恢复(续写/压缩/重试)→ 预算控制
🎯 一句话带走
记住这句话就够了:系统加固的核心是"让 Agent 更安全,更稳定,更可扩展"。权限检查防止危险操作,Hook 允许扩展而不改主循环,Memory 记住跨会话信息,Prompt 流水线组织输入,Error Recovery 让系统遇错不崩。
▶️ 下一步
下一篇文章我们将进入任务运行时部分,学习如何管理复杂项目、让慢任务异步执行、让时间触发工作。
相关文章:
关注我,一起探索 AI Agent 的世界!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)