MemfitAI安全性和缓存优化工程实践

admin 2026-07-18 05:00:22 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分享复杂自主Agent系统优化LLM前缀缓存的工程实践。针对上下文非线性累积致命中率仅1%的痛点,作者分三步解决:用哈希稳定派生Nonce替代随机Nonce以消除字节抖动;按稳定性将Prompt划分为高静态、半动态与动态段以隔离易变内容;对累积记忆采用时间分桶机制,以统一冻结时间点对齐各类记忆层边界。最终命中率升至60%,Token成本下降超50%,为AI系统缓存优化提供了可复用的分层与冻结架构方案。 综合评分: 90 文章分类: AI安全,安全开发,安全工具


cover_image

Memfit AI 安全性和缓存优化工程实践

原创

YAK YAK

Yak Project

2026年7月17日 17:35 湖南

在小说阅读器读本章

去阅读

一、从聊天应用到编程 Agent:上下文是怎么累积的

先从大家最熟悉的场景说起。

最基础的形式是”累积对话”——ChatGPT 网页版、各种基于 API 搭的客服 Bot、写作助手,底层 Prompt 的组织方式都是同一套范式:你发一条消息,AI 回一条,下一轮调用时把历史消息原样追加进 messages 数组发给模型。在这种模式下,Prompt 缓存优化几乎是自动的:system prompt 固定不变,历史消息天然构成稳定前缀,上游 LLM 的前缀缓存沿着对话历史一路命中,越聊越省钱。

再进一步是”工程 Agent”——比如 Codex CLI、Claude Code 这类工具。它们的上下文累积方式仍然是消息追加,但追加的不再只是用户输入和 AI 回复,还包括工具调用结果:Agent 读了一个文件,文件内容作为一条 assistant/tool message 追加进去;Agent 跑了一行命令,命令输出也追加进去;Agent 编辑了代码,diff 结果同样追加。本质上,它们把”工作过程”当作”对话历史”来累积——每一步操作的输入和输出都变成 messages 数组里的一条记录,下一轮 LLM 调用时把整段历史发给模型。

这种方式对 Prompt 缓存其实比较友好:system prompt(含工具定义)不变,历史消息按顺序追加,前缀天然稳定。每新增一条消息只是在前缀末尾追加内容,上游前缀缓存可以一路命中到上一次的最后一条消息。唯一的代价是上下文会越来越长,当超过模型窗口时需要做截断或摘要,但那是另一个问题。

我们的系统面临的情况又不一样。

我们的系统是一个多能力、带累积记忆、跑 DAG 任务图的自主 Agent 系统。它和前两者的关键区别在于:上下文不是沿一条线性对话流追加的,而是在不同任务上下文、不同子循环之间跳跃式拼装的。单次任务会发起几十到上百轮 LLM 调用,每轮调用的 Prompt 内容取决于当前执行到哪个任务、跑了哪个子循环、加载了哪些能力、Timeline 积累了多少——而不是简单地把”上一轮的对话”拼在前面。

具体来说,一次 LLM 调用的 Prompt 里混合着这些成分:

  • Agent 特质与系统角色指令:性格设定、输出格式硬规则、推理方法论、不同系统角色的核心指令。这些内容在理想情况下应该永远不变,但最初它们和 caller-specific 的内容(比如不同子循环的 OutputExample、能力开关)混在一起,没有明确的边界。
  • 能力清单:工具、AI 蓝图(Forge)、技能(Skill)、MCP Server 等。系统有仅内置就有数十种能力,可通过 search_capabilities 动态发现和按需加载。每次系统或用户加载新的能力,Prompt 中的能力清单就会变化。
  • 累积记忆:这是最棘手的部分。系统有四层记忆在 Prompt 里实时渲染——Timeline 时间线(每一步操作和观察结果的追加记录,一次任务可能积累几百到几千条)、Session Evidence(会话级证据,历轮工具执行后被显式写入的”已确认观测”)、Persistent Memory(跨会话的持久记忆)、Midterm Memory(Timeline 过长时自动压缩归档的旧片段)。这些记忆每轮都在增长。
  • 多任务上下文:在 Plan-Execute 模式下,用户的一个目标被拆成树状任务,按依赖关系拓扑执行。每个叶子任务带着父任务链、当前任务目标、执行指令跑一轮独立的 ReAct 循环。不同子任务的上下文内容各不相同。
  • 迭代反馈:ReAct 循环每轮的验证结果、下一步行动计划、工具调用返回值。这些内容每轮都不同。
  • 当前时间、工作目录等环境信息:看起来微不足道,时间的自然增长,它会让整段 Prompt 的字节 hash 每次都变。

问题在于,这些成分最初没有明确的界限——它们被拼成一段连续的文本发给 LLM,稳定的和易变的混在一起。上游前缀缓存沿着 Prompt 从头开始做字节级最长公共前缀匹配,一旦遇到第一个变化的字节,后面的内容无论多么稳定,都无法命中。

于是,Agent 每跑一步,Timeline 增长一条,能力清单可能变了一项,迭代反馈完全换了一轮——前缀缓存在很早就被击穿,后面几千 token 的稳定内容全部按全价计费。

最初的实测数据印证了这一点:一次 hostscan 任务跑下来,50~130 次 LLM 调用,缓存命中率只有 1.10%。99% 的 token 在按全价计费,而其中大部分本应是稳定内容。

这就是我们要解决的问题:在一个成分复杂、边界模糊、记忆持续增长的系统里,如何让上游 LLM 的字节级前缀缓存尽可能命中

二、问题一:缓存破坏的”单体成分”

在系统最初的状态里,Prompt 的各种成分没有明确界限,稳定的和易变的混在一起。但要拆解这个问题,首先要识别出哪些成分是”即使内容不变也会破坏缓存”的——它们是嵌入在 Prompt 字节里的定时炸弹。

YAK

2.1 随机 Nonce:防注入的代价

我们的系统大量使用 AITag(AI 标签)来隔离 Prompt 中的结构化内容,格式形如:

```
<|PARENT_TASK_a3x9k2|>... 父任务上下文 ...<|PARENT_TASK_END_a3x9k2|>
其中&nbsp;`a3x9k2`&nbsp;是 nonce。Nonce 的存在有明确的安全意义:它防止 Prompt Injection 攻击——如果攻击者构造的输入恰好包含&nbsp;`<|PARENT_TASK_xxx|>`&nbsp;这样的标签,就能伪造任务上下文。有了随机 nonce,攻击者无法猜测当前轮的标签字面量,注入的标签会被当成普通文本忽略。

问题出在 nonce 的生成方式:`utils.RandStringBytes(6)`,**每次 LLM 调用都随机生成一个新的**。

这意味着,即使两次调用的 Prompt body 完全相同——父任务链一样、当前任务目标一样、执行指令一样——但因为 nonce 变化了,整段文本的字节序列就不同了。上游前缀缓存沿着 Prompt 做字节级匹配,遇到 nonce 变化的位置就断了,后面所有内容全部 miss。

类似的问题还出现在其他"看起来无害"的单体成分上:

* **`time.Now().String()`**:时间戳,每秒都不同,让整段 Prompt 的字节 hash 每次都变;
* **工具调用缓存块里的 turn nonce**:每轮 LLM 调用的 nonce 直接嵌在&nbsp;`<|CACHE_TOOL_CALL_<nonce>|>`&nbsp;标签里,跨轮必然不同。

这些成分的共同特点是:**它们的相关有效内容本身可能不变,但嵌入的标识符每次都随机,导致字节序列不稳定**。

***YAK***

**2.2 稳定派生:让安全与缓存共存**

解决思路不是去掉 nonce(安全不能妥协),而是让 nonce 在"内容不变时也不变"。

我们设计了&nbsp;`StablePromptNonce`&nbsp;函数,用哈希把语义标识符映射成稳定的 6 字符 nonce:

func StablePromptNonce(parts …string) string {    h := fnv.New64a()    for i, p := range parts {        if i > 0 { h.Write([]byte{0}) }        h.Write([]byte(p))    }    v := h.Sum64()    // 映射到 6 字符稳定 nonce    …}

![](https://mmbiz.qpic.cn/sz_mmbiz_png/jibGAup6p72GlecXrKO6dkrKYSanumuG18HHrichvQqiaJZvHbSmmQvtksUgVPeenmOAEeHqwwcdrFupNdRP1GuXFN674dW57FBh9mhYkevlFM/640?wx_fmt=png&from=appmsg#imgIndex=3)

同一组 part&nbsp;**反复调用必返回相同 nonce**。

对于时间戳,解决方案更直接:把&nbsp;`time.Now()`&nbsp;从所有可能进入缓存前缀的段落中移除,时间信息只放在最易变的尾段。

对于工具调用缓存块,nonce 改用占位符字面量&nbsp;`"[current-nonce]"`&nbsp;渲染——LLM 看到方括号占位符理解为"应替换为当前轮 nonce",解析端通过双注册(turn nonce + 占位符字面量)兜底,任一种 LLM 行为都能正确解析。

这一步的效果是:缓存命中率从&nbsp;**1.10% 提升到 6.27%**。绝对值仍然不高,但这是从"几乎完全 miss"到"开始能命中"的质变——单体成分不再无差别地击穿前缀。

**三、问题二:如何把混乱的成分归入正确的段**

解决了单体成分的字节抖动之后,面对的是更大的问题:Prompt 的各种成分仍然混在一起,没有边界。上游缓存从 Prompt 开头开始做字节级最长公共前缀匹配,一旦遇到第一个变化的字节,后面再稳定的内容也无法命中。

解法是**按稳定性分层**——把成分按"跨调用不变的"和"每轮都变的"分开,让稳定的在前,易变的在后,在两者之间插入显式的缓存边界。

***YAK***

**3.1 High-Static 段:把"永远不变"的收整到一起**

第一类要收整的是**跨所有调用永远不变的内容**:**Agent 性格特质、输出格式硬规则、AITAG 协议、推理方法论**。这些内容无论当前跑的是哪个子循环、执行的是哪个任务,都不应该变化。

最初的问题是:这段内容里混进了**系统角色差异化**的字段。不同系统角色注入的 OutputExample 不同,能力配置在也会变。这些字段虽然每次调用**自身稳定,但跨角色切换时就会变**,把它们放在 high-static 里,任何 系统角色切换都会让整段内容漂移。

修复的第一步是**把 caller-specific 字段全部迁出**:OutputExample、能力开关、Schema 等 caller 维度才稳定的内容,下沉到后面的 semi-dynamic 段。high-static 段只保留真正跨所有 caller 都不变的内容。

修复的第二步是**提炼系统共享约束补足 token 数量**。主流模型厂商对显式缓存简历有最小窗口约束,最初的 high-static 段只有少量的内容,后续我们追加了 AITAG Protocol、Reasoning Protocol、Experiment Method Protocol 等跨所有 caller 都不变的方法论内容。

![](https://mmbiz.qpic.cn/mmbiz_png/jibGAup6p72FGicKq5qcf4AjqD3ncHw5QuribkwgVwINSdWNTbHY7Cfhe16CTAQbjCBsDuDcSIz5AHibiazJrg3E5OHjLCrCz7uuDzwhNP3DWia8c/640?wx_fmt=png&from=appmsg#imgIndex=4)

***YAK***

**3.2 Semi-Dynamic 段与 Dynamic 段:从混乱的角色 Prompt 中整合出标准结构**

在 high-static 之外,Prompt 中还有大量"同一 caller 内稳定但跨 caller 会变"的内容。最初这些内容和"每轮都变"的内容混在一起,没有边界。

以 ReAct 主循环为例,最初的 Prompt 里,Schema(当前可用 action 的 JSON Schema 定义)、OutputExample(输出格式示例)、Instruction(角色指令)和用户最新输入、迭代反馈数据、注入记忆等全部拼在一段连续文本里。Schema 在同一 loop 内每轮都一样,但和每轮都变的 ReactiveData 混排,导致缓存从第一个变化的字节处就断了。

整理后的方案是把这些内容分成两段:

**semi-dynamic 段**(同一 caller 内稳定):

* Instruction(当前系统的角色核心指令):同一 loop 内不变,但是切换角色会变化
* Schema(当前 action 的 JSON Schema):同一 loop 内不变
* OutputExample(输出格式示例):同一 caller 内不变
* CacheToolCall(工具调用缓存块):用占位符 nonce 渲染后跨轮字节稳定

**dynamic 段**(每轮都变):

* UserQuery(用户最新输入):每轮不同
* ReactiveData(迭代反馈、验证结果、工具返回值):每轮不同
* InjectedMemory(注入的记忆检索结果):每轮不同
* ExtraCapabilities(动态发现的能力):每轮可能不同

这一步的效果是:ReAct 主循环 5 次连续 chat 实现了 4 段全部 byte-identical,`prefix_hit_ratio = 100%`。全量命中率提升到&nbsp;**~25-30%**。

**四、问题三:累积记忆的滑动窗口困境**

前两个问题解决了"静态成分"和"成分边界"的问题,但系统中最大的不稳定因素还没处理:**累积记忆**。

***YAK***

**4.1 困境:Timeline 每步都在增长**

我们的系统有四层记忆在 Prompt 里实时渲染。其中 Timeline 时间线是最核心的:它记录了 Agent 每一步的操作和观察结果,一次任务可能积累几百到几千条记录。每跑一步,Timeline 就追加一条新记录——这意味着如果 Timeline 直接塞在 Prompt 里,前缀每轮都在变,缓存必 miss。

Session Evidence(会话级证据)也有类似的特性:历轮工具执行后被显式写入的"已确认观测",会随着任务推进不断累积。

这些记忆不可能简单丢弃——它们是 Agent 理解"已经做了什么"的事实依据。但也不能让它们无节制地击穿缓存。

***YAK***

**4.2 解法:Timeline 分桶 + Frozen/Open 边界**

关键观察是:**Timeline 的旧条目一旦写入就不再变化,只有最新写入的条目还在增长**。

我们利用&nbsp;`GroupByMinutes`&nbsp;机制把 Timeline 按绝对时间分桶(默认 3 分钟一个桶):

|  |  |  |
| --- | --- | --- |
| 桶类型 | 含义 | 字节稳定性 |
| Reducer block | 旧条目的压缩摘要 | 永久字节稳定 |
| Frozen interval block | 已封闭的时间桶(不再追加) | 永久字节稳定 |
| **末尾 Open interval block** | 当前还在写入的桶 | 会变 |

![](https://mmbiz.qpic.cn/mmbiz_png/jibGAup6p72GzMSicqsyfwQlw3GlTeknMpsupicv98dQuN0PWyjHBUXCFyiccgpjia0hFoSj705ib3VoibQ5hgP3P21WI2P0icqPR3V58sOTslibJ6dc/640?wx_fmt=png&from=appmsg#imgIndex=5)

这样,每次调用时 frozen 段的字节是稳定的(旧条目不变),只有 open 段在变——缓存边界精确地落在"最后一个还在变化的桶"之前。

***YAK***

**4.3 依托 Timeline 的冻结时间线,统一其他记忆的冻结标准**

Timeline 分桶解决了 Timeline 自身的缓存问题,但系统里还有其他累积记忆——Session Evidence(会话级证据)和 Session Artifacts(工作区产物)——它们同样在每轮增长,同样会击穿缓存。如果每种记忆各自维护一套冻结逻辑,不仅工程复杂,而且各自冻结的时机不同,frozen 段的边界会对不齐,反而互相破坏。

我们的做法是:**以 Timeline 的冻结时间点作为全局统一基准,让所有记忆内容蹭同一个时间点冻结**。

具体来说,我们设计了冻结相关内容有一个统一的入口。它先渲染 Timeline 的 frozen/open 分桶,从中提取出一个&nbsp;`FrozenTimeUnix`——即"最后一个已封闭桶的时间戳上限"。然后把这个时间戳传给 Session Evidence 和 Session Artifacts 的渲染函数:
func&nbsp;BuildPromptFrozenOpenMaterials(config *Config, openNonce ...string)&nbsp;PromptFrozenOpenMaterials {&nbsp; &nbsp;&nbsp;// 1. Timeline 先分桶,拿到冻结时间点&nbsp; &nbsp; timelineBlocks := RenderTimelineFrozenOpen(config.GetTimeline())&nbsp; &nbsp;&nbsp;// 2. 用同一个冻结时间点渲染 Artifacts&nbsp; &nbsp; artifactBlocks := RenderSessionArtifactsFrozenOpen(config, timelineBlocks.FrozenTimeUnix)&nbsp; &nbsp;&nbsp;// 3. 用同一个冻结时间点渲染 Evidence&nbsp; &nbsp; evidenceBlocks := config.GetSessionPromptState().&nbsp; &nbsp; &nbsp; &nbsp; GetSessionEvidenceFrozenOpenBlocks(timelineBlocks.FrozenTimeUnix, nonce)&nbsp; &nbsp;&nbsp;return&nbsp;PromptFrozenOpenMaterials{&nbsp; &nbsp; &nbsp; &nbsp; TimelineFrozen: &nbsp; &nbsp; &nbsp; &nbsp; timelineBlocks.Frozen,&nbsp; &nbsp; &nbsp; &nbsp; TimelineOpen: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; timelineBlocks.Open,&nbsp; &nbsp; &nbsp; &nbsp; TimelineFrozenTimeUnix: timelineBlocks.FrozenTimeUnix,&nbsp; &nbsp; &nbsp; &nbsp; SessionArtifactsFrozen: artifactBlocks.Frozen,&nbsp; &nbsp; &nbsp; &nbsp; SessionArtifactsOpen: &nbsp; artifactBlocks.Open,&nbsp; &nbsp; &nbsp; &nbsp; SessionEvidenceFrozen: &nbsp;evidenceBlocks.Frozen,&nbsp; &nbsp; &nbsp; &nbsp; SessionEvidenceOpen: &nbsp; &nbsp;evidenceBlocks.Open,&nbsp; &nbsp; &nbsp; &nbsp; ...&nbsp; &nbsp; }}

“`

这样做的好处是:当 Timeline 的一个时间桶从未封闭变成已封闭(从 open 变成 frozen)时,FrozenTimeUnix 向前推进,Session Evidence 和 Session Artifacts 也同步把此时点之前的内容冻结。三类记忆的 frozen/open 边界在同一轮调用中完全对齐,frozen 段作为一个整体进入缓存前缀,不会出现”Evidence 冻结了但 Timeline 还没冻结”的错位。

这一步的效果是:全量命中率提升到 ~60%。

总结

整个过程解决了三个层面的问题,命中率从 1% 一路提升到 60%:

| | | | | | — | — | — | — | | 阶段 | 解决的问题 | 关键动作 | 命中率 | | 修复单体成分 | Nonce 随机、时间戳抖动等”内容不变但字节变”的隐患 | 稳定派生 Nonce、移除时间戳 | 1% → 6% | | 分段分层 | 稳定内容和易变内容混排,前缀被第一个变化字节击穿 | high-static 收整(迁出 caller-specific 字段 + 补足 1500 token)、semi/dynamic 拆分、Hijacker 按边界切割并打 cache_control | 6% → ~25% | | 累积记忆冻结 | Timeline / Evidence / Artifacts 每轮增长,前缀永远在变 | Timeline 分桶 + Frozen/Open 边界、以 FrozenTimeUnix 统一三类记忆的冻结时机、Frozen Partition 冻结 plan 产物 | ~25% → ~60% |

最终效果:命中 token 按 input 单价的 10% 计费,60% 命中率意味着token 成本下降约 50-60%

END

更新记录

Yakit 1.4.8-0717

  1. 修复 HTTPFlow 表格加载慢的问题

  2. 修复 WebFuzzer hex 渲染值不一致问题

  3. AI Agent 工具卡片展示和布局优化,失败卡片样式优化

  4. 任务/子 Agent 任务定位高亮复用化与样式优化

  5. 插件 Tab 新增历史记录,插件执行结果优化

  6. YakitEditor 右键菜单支持打开/关闭二进制组件

  7. OpenAPI 文档解析新增进度显示与取消功能

  8. 修复 IM 机器人删除按钮失效

  9. 连接页面支持翻译,导航栏导入资源文案改为导入插件/流量

  10. 字典/代理/热加载管理合并为资源管理

  11. Agent 增加对话模式选择,新增 Goal 和 Multi-Agent

  12. Yak Runner 右侧 AI 改代码优化,增加片段更改

Memfit  v11.0.3-0717

  1. AI Agent 工具卡片展示和布局优化,失败卡片样式优化

  2. 任务/子Agent 任务定位高亮复用化与样式优化

  3. 优化底层数据逻辑

  4. Agent 增加对话模式选择,新增 Goal 和 Multi-Agent

Yaklang v 1.4.8-beta5

  1. 新增 MCP 工具集(chaos maker、fingerprint、http builder 等)

  2. 修复大型项目 SyntaxFlow 扫描的内存占用与卡死问题

  3. 新增代码审计子智能体支持,增强 AI ReAct 循环与工具调用能力

  4. 新增 AI 工具名称双语支持

  5. 增强 WebSocket 错误处理与转发能力

  6. 修复 CVE 查询响应分页为空的问题

  7. 新增 SyntaxFlow 规则

YAK官方资源

Yak 语言官方教程:

https://yaklang.com/docs/intro/

Yakit 视频教程:

https://space.bilibili.com/437503777

Github下载地址:

https://github.com/yaklang/yakit

Yakit官网下载地址:

https://yaklang.com/

Yakit安装文档:

https://yaklang.com/products/download_and_install

Yakit使用文档:

https://yaklang.com/products/intro/

常见问题速查:

https://yaklang.com/products/FAQ


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Yak Project YAK YAK《Memfit AI 安全性和缓存优化工程实践》

评论:0   参与:  0