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。