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

相关推荐
Wang's Blog2 小时前
Java 接入Redis: 五大数据类型与存储结构选型
java·服务器·redis
Wang's Blog2 小时前
Java 接入Redis: 字符串与哈希类型操作命令
java·服务器·redis
智慧物业老杨6 小时前
物业数字化落地思考:真正的转型,是底层数据秩序的重构
java·大数据·人工智能·微服务·系统架构
萧西待水7 小时前
奥赛一本通 1447 靶形数独
算法·深度优先
星空9 小时前
金蝶苍穹build.gradle配置
java·build.gradle
麻雀飞吧9 小时前
先判断工具用来学习、开发还是执行
人工智能·python
泯泷9 小时前
那段文字是谁删的?Yjs 14 正式版之前,一套删除归属方案的实现与边界
前端·javascript·算法
hold?fish:palm10 小时前
34 合并K个升序链表
javascript·算法·链表
人邮异步社区10 小时前
学习Python的最佳学习路径是什么?
python·程序员