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|>
其中 a3x9k2 是 nonce。Nonce 的存在有明确的安全意义:它防止 Prompt Injection 攻击——如果攻击者构造的输入恰好包含 <|PARENT_TASK_xxx|> 这样的标签,就能伪造任务上下文。有了随机 nonce,攻击者无法猜测当前轮的标签字面量,注入的标签会被当成普通文本忽略。
问题出在 nonce 的生成方式:utils.RandStringBytes(6),每次 LLM 调用都随机生成一个新的。
这意味着,即使两次调用的 Prompt body 完全相同——父任务链一样、当前任务目标一样、执行指令一样——但因为 nonce 变化了,整段文本的字节序列就不同了。上游前缀缓存沿着 Prompt 做字节级匹配,遇到 nonce 变化的位置就断了,后面所有内容全部 miss。
类似的问题还出现在其他\"看起来无害\"的单体成分上:
time.Now().String():时间戳,每秒都不同,让整段 Prompt 的字节 hash 每次都变;- 工具调用缓存块里的 turn nonce:每轮 LLM 调用的 nonce 直接嵌在
<|CACHE_TOOL_CALL_<nonce>|>标签里,跨轮必然不同。
这些成分的共同特点是:它们的相关有效内容本身可能不变,但嵌入的标识符每次都随机,导致字节序列不稳定。
YAK
2.2 稳定派生:让安全与缓存共存
解决思路不是去掉 nonce(安全不能妥协),而是让 nonce 在\"内容不变时也不变\"。
我们设计了 StablePromptNonce 函数,用哈希把语义标识符映射成稳定的 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 ...}
同一组 part 反复调用必返回相同 nonce。
对于时间戳,解决方案更直接:把 time.Now() 从所有可能进入缓存前缀的段落中移除,时间信息只放在最易变的尾段。
对于工具调用缓存块,nonce 改用占位符字面量 \"[current-nonce]\" 渲染——LLM 看到方括号占位符理解为\"应替换为当前轮 nonce\",解析端通过双注册(turn nonce + 占位符字面量)兜底,任一种 LLM 行为都能正确解析。
这一步的效果是:缓存命中率从 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 都不变的方法论内容。
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%。全量命中率提升到 ~25-30%。
四、问题三:累积记忆的滑动窗口困境
前两个问题解决了\"静态成分\"和\"成分边界\"的问题,但系统中最大的不稳定因素还没处理:累积记忆。
YAK
4.1 困境:Timeline 每步都在增长
我们的系统有四层记忆在 Prompt 里实时渲染。其中 Timeline 时间线是最核心的:它记录了 Agent 每一步的操作和观察结果,一次任务可能积累几百到几千条记录。每跑一步,Timeline 就追加一条新记录——这意味着如果 Timeline 直接塞在 Prompt 里,前缀每轮都在变,缓存必 miss。
Session Evidence(会话级证据)也有类似的特性:历轮工具执行后被显式写入的\"已确认观测\",会随着任务推进不断累积。
这些记忆不可能简单丢弃——它们是 Agent 理解\"已经做了什么\"的事实依据。但也不能让它们无节制地击穿缓存。
YAK
4.2 解法:Timeline 分桶 + Frozen/Open 边界
关键观察是:Timeline 的旧条目一旦写入就不再变化,只有最新写入的条目还在增长。
我们利用 GroupByMinutes 机制把 Timeline 按绝对时间分桶(默认 3 分钟一个桶):
| 桶类型 | 含义 | 字节稳定性 |
| Reducer block | 旧条目的压缩摘要 | 永久字节稳定 |
| Frozen interval block | 已封闭的时间桶(不再追加) | 永久字节稳定 |
| 末尾 Open interval block | 当前还在写入的桶 | 会变 |
这样,每次调用时 frozen 段的字节是稳定的(旧条目不变),只有 open 段在变——缓存边界精确地落在\"最后一个还在变化的桶\"之前。
YAK
4.3 依托 Timeline 的冻结时间线,统一其他记忆的冻结标准
Timeline 分桶解决了 Timeline 自身的缓存问题,但系统里还有其他累积记忆——Session Evidence(会话级证据)和 Session Artifacts(工作区产物)——它们同样在每轮增长,同样会击穿缓存。如果每种记忆各自维护一套冻结逻辑,不仅工程复杂,而且各自冻结的时机不同,frozen 段的边界会对不齐,反而互相破坏。
我们的做法是:以 Timeline 的冻结时间点作为全局统一基准,让所有记忆内容蹭同一个时间点冻结。
具体来说,我们设计了冻结相关内容有一个统一的入口。它先渲染 Timeline 的 frozen/open 分桶,从中提取出一个 FrozenTimeUnix——即\"最后一个已封闭桶的时间戳上限\"。然后把这个时间戳传给 Session Evidence 和 Session Artifacts 的渲染函数:
func BuildPromptFrozenOpenMaterials(config *Config, openNonce ...string) PromptFrozenOpenMaterials { // 1. Timeline 先分桶,拿到冻结时间点 timelineBlocks := RenderTimelineFrozenOpen(config.GetTimeline()) // 2. 用同一个冻结时间点渲染 Artifacts artifactBlocks := RenderSessionArtifactsFrozenOpen(config, timelineBlocks.FrozenTimeUnix) // 3. 用同一个冻结时间点渲染 Evidence evidenceBlocks := config.GetSessionPromptState(). GetSessionEvidenceFrozenOpenBlocks(timelineBlocks.FrozenTimeUnix, nonce) return PromptFrozenOpenMaterials{ TimelineFrozen: timelineBlocks.Frozen, TimelineOpen: timelineBlocks.Open, TimelineFrozenTimeUnix: timelineBlocks.FrozenTimeUnix, SessionArtifactsFrozen: artifactBlocks.Frozen, SessionArtifactsOpen: artifactBlocks.Open, SessionEvidenceFrozen: evidenceBlocks.Frozen, SessionEvidenceOpen: evidenceBlocks.Open, ... }}
这样做的好处是:当 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
-
修复 HTTPFlow 表格加载慢的问题
-
修复 WebFuzzer hex 渲染值不一致问题
-
AI Agent 工具卡片展示和布局优化,失败卡片样式优化
-
任务/子 Agent 任务定位高亮复用化与样式优化
-
插件 Tab 新增历史记录,插件执行结果优化
-
YakitEditor 右键菜单支持打开/关闭二进制组件
-
OpenAPI 文档解析新增进度显示与取消功能
-
修复 IM 机器人删除按钮失效
-
连接页面支持翻译,导航栏导入资源文案改为导入插件/流量
-
字典/代理/热加载管理合并为资源管理
-
Agent 增加对话模式选择,新增 Goal 和 Multi-Agent
-
Yak Runner 右侧 AI 改代码优化,增加片段更改
Memfit v11.0.3-0717
-
AI Agent 工具卡片展示和布局优化,失败卡片样式优化
-
任务/子Agent 任务定位高亮复用化与样式优化
-
优化底层数据逻辑
-
Agent 增加对话模式选择,新增 Goal 和 Multi-Agent
Yaklang v 1.4.8-beta5
-
新增 MCP 工具集(chaos maker、fingerprint、http builder 等)
-
修复大型项目 SyntaxFlow 扫描的内存占用与卡死问题
-
新增代码审计子智能体支持,增强 AI ReAct 循环与工具调用能力
-
新增 AI 工具名称双语支持
-
增强 WebSocket 错误处理与转发能力
-
修复 CVE 查询响应分页为空的问题
-
新增 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
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论