Prompt Cache、Token 成本与 Plan Compiler 的工程设计

1. 问题是怎么暴露出来的

我介绍了自己在 Agent 系统中的 Tool Router 和 Plan Compiler 设计。大致流程是:

复制代码
LLM
 ↓
提出 Tool Proposal
 ↓
Tool Router
 ↓
Plan Compiler
 ↓
依赖检查 / 权限检查 / DAG 编译
 ↓
Tool Execution
 ↓
Observation
 ↓
LLM

面试官随后追问了一个之前我没有认真考虑过的问题:

如果 LLM 认为只需要 A、B 两个工具,但是 Plan Compiler 根据依赖关系发现还必须执行工具 C,应该怎么处理?如果重新把 C 告诉 LLM,会不会影响缓存命中和 Token 消耗?

我当时主要从依赖和权限检查 角度考虑了问题,但没有系统考虑 Prompt Cache 和推理成本。这个追问让我意识到:Agent 系统设计不仅要关注"能不能跑"和"安不安全",还必须关注一次任务到底调用多少次模型、消耗多少 Token、Prompt 是否稳定、缓存是否能够复用。

下面把这一块系统补齐。


2. 一次 LLM 请求到底在消耗什么?

先看一个典型 Agent 请求:

复制代码
System Prompt
+
Skill / Domain Instructions
+
Tool Schemas
+
Conversation History
+
Retrieved Evidence
+
Current User Query

模型处理这段输入时,大体可以理解成两个阶段:

复制代码
Input Tokens
     ↓
Prefill
     ↓
生成第一个 Token
     ↓
Decode
     ↓
一个一个生成 Output Tokens

其中:

  • Input Token:输入给模型的上下文;

  • Output Token:模型生成的内容;

  • Prefill:模型处理整段输入;

  • Decode:逐 Token 生成输出。

如果一个 Agent 要经过 5 次 LLM 调用,即使用户只问了一句话,也可能重复处理大量:

复制代码
System Prompt
Tool Schema
Skill
History
Evidence

因此 Agent 的真实成本不能只看最终答案长度。


3. 什么是 Prompt Cache?

很多 LLM 推理系统都会对重复的 Prompt 前缀进行缓存。比如第一次请求:

复制代码
System Prompt
Skill
Tool A Schema
Tool B Schema
History
User Query

第二次请求仍然是:

复制代码
System Prompt
Skill
Tool A Schema
Tool B Schema
History
新的 Observation

那么前面的:

复制代码
System Prompt
Skill
Tool A Schema
Tool B Schema
History

大部分内容没有变化。

推理系统就有机会复用之前已经计算过的前缀,而不用重新完成全部 Prefill。

这就是 Prompt Cache,也经常称为:

复制代码
Prefix Cache
Prompt Caching
Cached Input Tokens

不同模型服务对缓存规则、粒度和计费方式并不完全相同,因此工程上不能假设"只要文本相似就一定命中";但核心思想都是:

稳定且完全一致的 Prompt 前缀越长,可复用的计算通常越多。


4. "Token 命中"不是关键词命中

这里非常容易误解。Prompt Cache 的 Token 命中不是:

复制代码
上一轮出现了 VPN
这一轮也出现 VPN
→ VPN 这个 Token 命中

不是这样的。它更接近:

复制代码
请求1:
A B C D E F G H

请求2:
A B C D E F X Y

前面的:

复制代码
A B C D E F

是一致的 prefix,因此有机会复用。但是从:

复制代码
X

开始,Token 序列已经不同。

所以缓存关注的是:

连续的、稳定的 Prefix。

而不是:

"两个 Prompt 有多少相同单词"。


5. 为什么动态 Tool Schema 容易影响缓存?

假设原来的 Prompt 是:

复制代码
System Prompt

Skill

Tool A:
  ...

Tool B:
  ...

Tool D:
  ...

Conversation

下一轮突然加入 Tool C:

复制代码
System Prompt

Skill

Tool A:
  ...

Tool B:
  ...

Tool C:       ← 新加入
  ...

Tool D:
  ...

Conversation

从 Tool C 插入的位置开始,后面的 Token 序列就发生了变化。

于是可能出现:

复制代码
System Prompt   ✅ cache
Skill           ✅ cache
Tool A          ✅ cache
Tool B          ✅ cache

Tool C          ❌ new

Tool D          ❌ prefix 已经变化
Conversation    ❌
...

这就是为什么:

动态修改靠前的 Prompt 内容,可能降低 Prefix Cache 的利用率。

注意是"可能",因为具体缓存行为依赖模型服务实现。


6. Cache Miss 会带来什么?

主要有三个影响。

6.1 Prefill 计算增加

如果本来:

复制代码
10000 input tokens

其中:

复制代码
8000 cached

真正重新计算的内容就相对较少。

如果因为 Tool Schema 变化,只有:

复制代码
3000 cached

那么更多 Token 需要重新做 Prefill。


6.2 延迟可能增加

LLM 的首 Token 延迟通常与 Input Context 长度有关。

上下文越长:

复制代码
Prefill workload ↑

如果缓存不能利用:

复制代码
Time To First Token

往往也会受到影响。


6.3 成本可能增加

具体计费规则取决于模型供应商,但很多推理服务会区分:

复制代码
cached input
uncached input
output

因此缓存利用率也可能直接影响调用费用。


7. 但是更大的成本,往往不是 Cache Miss,而是"多调用了一次 LLM"

这一点特别重要。考虑下面这个设计。

LLM 第一次:

复制代码
我需要:
Tool A
Tool B

Plan Compiler:

复制代码
A 实际还依赖 C

然后系统再告诉 LLM:

复制代码
你还缺少 C,请重新规划。

于是:

复制代码
LLM Call #1
        ↓
Compiler
        ↓
LLM Call #2

第二次模型调用往往又需要发送:

复制代码
System Prompt
Skill
Tools
History
Previous Plan
Compiler Feedback

因此即使 Prompt Cache 命中了很大一部分:

多一次 LLM Round Trip 本身就一定会增加 Token、延迟和调用成本。

所以这里必须区分两个问题。

额外 LLM Call

通常一定增加:

复制代码
调用次数
Token 消耗
网络延迟
模型推理延迟

Prompt Cache Miss

是在此基础上进一步导致:

复制代码
更多 Prefill
更少 Cached Tokens
潜在更高成本

因此:

Extra Call 和 Cache Miss 是两个问题,不能混为一谈。


8. Plan Compiler 为什么应该尽量 deterministic?

回到最开始的问题。

假设 LLM 输出:

复制代码
A
B

而 Server Registry 定义:

复制代码
A dependsOn C

那么最合理的系统并不是:

复制代码
LLM:A、B

Compiler:
你还需要 C

LLM:
好的,那我改成 A、B、C

因为这件事根本不需要语言模型推理。

更好的架构应该是:

复制代码
            LLM
             │
             │ Proposal
             ▼
           A, B
             │
             ▼
      Trusted Tool Registry
             │
        A dependsOn C
             │
             ▼
        Plan Compiler
             │
      Dependency Closure
             │
             ▼
          A, B, C
             │
      ┌──────┴──────┐
      │             │
 Permission      Cycle Check
      │             │
 Max Calls      Side Effect
      │             │
      └──────┬──────┘
             ▼
           DAG
             │
             ▼
          Execute

也就是说:

Dependency Completion 应该由代码完成,而不是重新调用 LLM。


9. 什么是 Dependency Closure?

假设 Registry:

复制代码
A dependsOn C
C dependsOn D
B has no dependency

LLM 只提出:

复制代码
A
B

Plan Compiler 自动展开:

复制代码
A
↓
C
↓
D

最终工具集合:

复制代码
A
B
C
D

执行 DAG 可能是:

复制代码
D
↓
C
↓
A

B 可以独立执行

这个过程就是依赖闭包的一部分。它完全可以由:

复制代码
DFS
BFS
Topological Sort
Registry Lookup

等确定性算法完成。不需要让 LLM 再"思考一次"。


10. 如果补出来的 C 没有权限怎么办?

这又是一个非常重要的安全边界。

假设:

复制代码
A dependsOn C

但是用户没有:

复制代码
permission(C)

错误设计:

复制代码
Compiler:
C 没权限。

LLM:
那我换一种 Plan 绕过去试试。

这可能产生一个严重问题:

LLM 开始尝试绕过 Server Policy。

更合理的是:

复制代码
LLM proposes A

      ↓

Compiler discovers C

      ↓

C permission denied

      ↓

Reject Plan

即:

必要依赖没有权限,则整个 Plan Fail Closed。

而不是:

"再让 LLM 想想办法"。


11. 为什么这同时是安全优化和成本优化?

非常有意思的是:

复制代码
不重新询问 LLM

同时改善了两件事。

安全

LLM 无法通过重新规划绕过:

复制代码
Permission
Dependency
Policy
State
Max Calls

成本

减少:

复制代码
LLM Calls
Input Tokens
Output Tokens
Latency
Prompt Changes

所以:

好的安全架构有时也是好的成本架构。


12. LLM 可以提议,但不能授权

Agent 系统里非常重要的一条原则:

复制代码
LLM can propose,
but cannot authorize.

例如 LLM 输出:

复制代码
{
  "tool": "dispatch_work_order",
  "arguments": {
    "alert_id": "123",
    "priority": "HIGH"
  }
}

这只能代表:

模型建议调用这个 Tool。

它不代表:

系统允许调用。

Server 仍然需要检查:

复制代码
Tool 是否存在
Schema 是否合法
User 是否有权限
当前状态是否合法
调用次数是否超限
依赖是否满足
Side Effect 是否允许
是否需要人工审批

13. Function Calling、Router 和 Compiler 的职责应该怎么分?

我现在更倾向于把三层明确拆开。

Function Calling

解决:

LLM 想调用什么?

例如:

复制代码
tool = analyze_flow
args = {...}

本质是:

复制代码
Structured Proposal

Tool Router

解决:

当前这轮允许 LLM 看见什么?

例如:

复制代码
系统注册了 100 个 Tool

当前任务只允许:
search_knowledge
analyze_flow

于是只暴露:

复制代码
2 个 Tool

Router 做的是:

复制代码
Capability Exposure
Minimum Privilege
Admission

Plan Compiler

解决:

LLM 的 Proposal 最终应该怎么执行?

包括:

复制代码
Dependency Closure
Permission
Cycle Detection
Topology
Serial / Parallel
Failure Policy
maxCalls
Side Effects
Concurrency

所以可以总结:

复制代码
Function Calling
      ↓
"What does LLM want?"

Tool Router
      ↓
"What may LLM access?"

Plan Compiler
      ↓
"What may actually execute?"

14. 为什么 Tool Router 也和 Token 成本相关?

假设系统有:

复制代码
100 tools

如果全部 Tool Schema 都塞给模型:

复制代码
System
+
100 Tool Schemas
+
Conversation

可能本身就是一个巨大的 Prompt。而实际上某个用户问题:

复制代码
"分析一下这个 VPN Flow"

可能只需要:

复制代码
search_vpn_knowledge
analyze_flow

于是 Router 可以把:

复制代码
100 Tools

缩成:

复制代码
2 Tools

这样同时带来:

复制代码
Token ↓
Tool Confusion ↓
Attack Surface ↓
Tool Selection Difficulty ↓

所以:

最小 Tool Exposure 不仅是权限安全问题,也是 Context Engineering 问题。


15. 但动态 Tool Exposure 又可能影响 Cache,怎么办?

这里就出现了一个工程 trade-off:

复制代码
暴露所有 Tools

优点:

复制代码
Prompt 更固定
可能更 cache-friendly

缺点:

复制代码
Token 多
攻击面大
Tool Selection 难

而:

复制代码
动态暴露少量 Tools

优点:

复制代码
Prompt 短
权限边界好
Selection 简单

缺点:

复制代码
Tool Schema 可能随请求变化
Cache Prefix 稳定性下降

因此不是简单地说:

"缓存越高越好。"

而应该综合:

复制代码
Token 数量
Cache Hit
安全性
Tool Accuracy
延迟

16. 一个更合理的 Prompt 组织方式

为了提高缓存友好性,可以尽量保持前缀稳定。例如:

复制代码
┌───────────────────────┐
│ Stable System Prompt  │
├───────────────────────┤
│ Stable Safety Policy  │
├───────────────────────┤
│ Stable Skill          │
├───────────────────────┤
│ Dynamic Tool Context  │
├───────────────────────┤
│ Dynamic Evidence      │
├───────────────────────┤
│ Conversation          │
├───────────────────────┤
│ Current Query         │
└───────────────────────┘

前面:

复制代码
System
Policy
Skill

尽量保持稳定。后面:

复制代码
Tools
Evidence
Query

允许动态变化。

这样即使动态部分变化:

前面的 Stable Prefix 仍有机会被缓存。


17. 不要把所有东西都塞进 Prompt

这是我这次复盘后特别深的一个体会。以前做 Agent 容易产生一种思路:

"模型不知道?那我把它放进 Context。"

最后变成:

复制代码
巨大 System Prompt
+
巨大 Skill
+
100 个 Tools
+
所有聊天记录
+
几十个 RAG Chunk
+
所有业务状态

实际上更好的设计应该是:

复制代码
该由数据库查的 → 数据库查
该由规则判断的 → 规则判断
该由 Compiler 算的 → Compiler 算
该由状态机约束的 → 状态机约束
真正需要语义理解的 → 才交给 LLM

一句话:

Context Window 不是系统内存,更不是数据库。


18. 每次设计 LLM Call,都应该问自己几个问题

以后设计 Agent,我会固定检查下面这些问题:

这个步骤真的需要 LLM 吗?例如:

复制代码
DAG 拓扑排序
权限检查
Schema 校验
maxCalls
状态迁移

答案显然是:

不需要。


能不能 deterministic?

如果可以通过:

复制代码
Rule
Registry
Database
Compiler
State Machine

解决,就优先不用模型。


这一步会不会新增一次 Model Round Trip?

例如:

复制代码
Compiler:
你少了 C

→ 再问 LLM

就应该警觉。


Prompt 前缀稳定吗?

重点检查:

复制代码
System Prompt
Skill
Tool Schema
Policy

是否每轮不断变化。


动态信息必须放进模型吗?

例如:

复制代码
Tool Permission Matrix

很多时候模型根本不需要知道。

让 Server 检查即可。


19. Prompt Cache 和 KV Cache 不要混淆

面试时如果继续深入,很可能还会遇到 KV Cache。两者虽然都叫 Cache,但侧重点不同。

KV Cache

主要发生在一次模型生成过程中。

模型已经处理:

复制代码
A B C D

现在要生成 E。

不需要每次重新计算:

复制代码
A B C D

的全部 Attention Key/Value。

因此保存:

复制代码
K
V

供后续 Decode 使用。

它主要优化:

单次生成。


Prompt / Prefix Cache

重点是:

跨请求复用重复 Prompt 前缀。

比如 Agent 多轮调用:

复制代码
Call 1:
System + Skill + Tools + Query

Call 2:
System + Skill + Tools + Observation

Call 3:
System + Skill + Tools + Observation

前面的稳定部分可以尝试复用。

简单记:

复制代码
KV Cache
→ 一次生成内部

Prompt Cache
→ 多个请求之间的重复前缀

实际底层实现可能存在联系,但面试时这样区分足够清楚。


20. Agent 性能优化不能只看模型大小

以前容易觉得:

复制代码
模型慢
→ 换一个更小模型

但真实 Agent 延迟可能来自:

复制代码
LLM Call × 8
Tool Call × 12
RAG × 3
Huge Context
Cache Miss
Sequential Execution

比如:

复制代码
每次 LLM 1 秒

如果调用:

复制代码
8 次

系统就已经可能接近:

复制代码
8 秒+

所以优化 Agent 应该看:

TotalLatency≈∑LLMLatency+∑ToolLatency+RetrievalLatency+NetworkOverheadTotalLatency \approx \sum LLMLatency + \sum ToolLatency + RetrievalLatency + NetworkOverhead

因此:

减少一次没有必要的 LLM Call,有时候比把单次模型加速 10% 更有价值。


21. Agent 的成本也应该系统化观察

一个比较实用的观测指标可以包括:

复制代码
LLM Calls / Task

Input Tokens / Task

Output Tokens / Task

Cached Input Tokens

Cache Hit Ratio

Tool Calls / Task

Tool Retry Count

RAG Calls / Task

Time To First Token

End-to-End Latency

Success Rate

例如:

复制代码
Task A

LLM Calls        = 4
Input Tokens     = 18,500
Cached Tokens    = 12,000
Output Tokens    = 1,800
Tool Calls       = 5
Tool Retries     = 1
Total Latency    = 6.2s

这样才能知道:

成本到底花在哪里。


22. 一个非常实用的 Agent 优化优先级

我现在会按照下面顺序考虑优化:

复制代码
第一步
减少不必要的 LLM Calls

        ↓

第二步
减少不必要的 Context

        ↓

第三步
保持 Stable Prefix

        ↓

第四步
减少不必要的 Tool Calls

        ↓

第五步
并行执行真正无依赖的 Tool

        ↓

第六步
再考虑模型大小 / 推理速度

很多系统一开始就:

换模型、量化模型。

但可能真正的问题是:

一个任务调用了模型十几次。


23. 为什么"确定性核心 + Agent 边缘"越来越重要

我越来越认可这样一种 Agent 架构:

复制代码
         LLM
      Reasoning
      Proposal
          │
          ▼
──────────────────────
 Deterministic Core
──────────────────────
 Schema Validation
 Permission
 Registry
 Dependency
 DAG
 State Machine
 Idempotency
 Audit
 Rate Limit
 Retry Policy
──────────────────────
          │
          ▼
        Tools

也就是:

LLM 做它擅长的理解、推理和生成。

而:

软件系统做它擅长的约束、验证和执行。

可以概括成:

Agentic Edge,Deterministic Core。

这不仅提高安全性,也能改善:

复制代码
稳定性
可复现性
延迟
Token 成本
缓存利用率

24. 回到最开始那道面试题,现在应该怎么回答?

面试官:

LLM 提出了 A、B,但 Plan Compiler 发现 A 依赖 C。如果 C 没权限怎么办?重新告诉 LLM 会不会影响缓存和 Token?

现在我的答案会是:

如果 C 是服务端 Registry 声明的必要依赖,Plan Compiler 会 deterministic 地完成 dependency closure,而不是重新调用 LLM。

如果 C 通过权限、调用预算、状态和安全策略检查,就把 C 加入 DAG;如果 C 不满足执行条件,那么依赖 C 的 A 也不能执行,整个 Plan 按 fail-closed 策略拒绝。

我不会默认把"缺少 C"重新返回给 LLM,因为依赖关系本身是服务端确定性知识,不需要模型二次推理,而且重新调用模型会增加 Token 和端到端延迟。

如果二次调用时还动态修改了 Prompt 前部的 Tool Schema,例如把 C 加进 Tool Definition,那么稳定 Prompt Prefix 发生变化,还可能降低 Prompt Cache 的利用率,导致更多 Input Token 重新 Prefill。

所以设计原则是:LLM 负责 proposal,Compiler 负责 dependency completion,Server 负责 authorization。


25. 最后总结

这次面试让我补上了以前设计 Agent 时比较容易忽视的一层:以前考虑的是:

复制代码
能不能完成任务?
权限安全吗?
DAG 正确吗?

现在还要继续考虑:

复制代码
为什么这里一定要调用 LLM?
能不能用 deterministic logic?
一个任务调用多少次模型?
Context 为什么这么长?
Prompt Prefix 稳不稳定?
Cached Tokens 有多少?
Tool Schema 是否频繁变化?

最终我认为一个成熟的 Agent 系统至少需要同时优化五个维度:

维度 典型问题
Correctness DAG 和依赖是否正确
Security 是否越权、是否最小权限
Reliability 幂等、Retry、Failure Policy
Cost LLM Call 和 Token 是否过多
Latency Cache、并行度、Tool/LLM Round Trip

而其中最值得记住的一条原则是:

能由确定性系统解决的问题,不要为了"Agent 化"再调用一次 LLM。

对于 Plan Compiler 来说尤其如此:

依赖缺失,Compiler 补;
依赖无权限,Plan 拒绝;
不要为了确定性依赖,再问一次 LLM。

这次面试让我意识到,Agent 工程的优化已经不只是 Prompt Engineering,而是在逐渐进入真正的 LLM Systems Engineering。

相关推荐
懂点Tech5 小时前
ctfshow misc28:GIF 高度修改解题与脚本实现
开发语言·windows·python
comedate5 小时前
SMLib 的 Examples 导读:PyAPI / SM_API / SMLib 三组示例怎么读
python·cad·smlib·examples·sm_api
微尘hjx5 小时前
【ByteTrack 04】多轨迹算法ByteTrack改进点
算法·yolo·目标检测·目标跟踪·bytetrack·ultralytics·车跟踪
云和数据.ChenGuang5 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
摇滚侠6 小时前
《On Java 中文版 基础卷》阅读笔记 实现隐藏 12
java·数据库·笔记
川石课堂软件测试6 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
万化冥合7 小时前
PyBox屏幕键盘如何辅助演示键盘操作?
python·计算机外设
车软学堂7 小时前
从测试转开发,到底要补什么
网络·人工智能·python·功能测试·单片机·车载系统·自动化
天空依然蔚蓝7 小时前
分享一个自动检测网站在线状态的python程序
python·运维开发·1024程序员节
岁月宁静7 小时前
一个 AI 项目从本地到上线,要经过哪些步骤?全流程讲清楚
python·docker·agent