第7章 意图识别与决策中枢
来源:https://ai-agent-guide.xiaofuge.cn/chapters/ch06-brain-intent-router.html 所属:第二篇-Agent的大脑
前两章讲了 Agent 的"推理引擎"(ReAct 及变体)和"记忆系统"(上下文管理)。但 Agent 还需要一个核心能力——理解用户到底想做什么,然后决定怎么做。这就是意图识别与决策中枢。
"Agent 的大脑不只是推理,还包括:理解意图 → 规划任务 → 调度工具 → 管理上下文 → 控制循环。这五件事合在一起,才是完整的'决策中枢'。"
7.1 为什么需要意图识别
用户说"帮我查下北京天气",Agent 需要先理解用户到底想做什么,才能选到正确的工具、走正确的执行路径。意图识别是 Agent 决策链的第一步,也是最重要的一步——意图错了,后面全错。
意图识别在 Agent 大脑中的位置
用户输入 → 🧠 意图识别
→ 任务分类 → 工具路由 → 执行调度
意图识别是决策链的起点,决定了 Agent 后续的所有行为
三种意图识别方法
Agent 接收用户输入后,需要判断用户的真实意图。工程上有三种主流方法,各有优劣:
意图识别的三种方法对比 | 方法 | 原理 | 优点 | 缺点 | 适用场景 | | --- | --- | --- | --- | --- | | 关键词匹配 | 匹配预设关键词,如"天气"→查询意图 | 速度快,成本低 | 粗粒度,误判率高 | 简单指令型交互 | | 语义理解 | 通过 LLM 深层理解用户表达的真实意图 | 精准,能理解隐含意图 | 成本高,延迟大 | 复杂多意图场景 | | 模式识别 | 基于历史对话模式匹配相似意图 | 利用经验,越用越准 | 依赖数据量,新意图难识别 | 高频重复场景 | 实际工程中,三者通常组合使用:先用关键词匹配快速筛选常见意图,命中后直接走快速路径;没命中则交给 LLM 做语义理解;长期积累的模式数据用于优化匹配规则。这就是分层意图识别策略。
# 分层意图识别:关键词 → 语义 → 模式
def identify_intent(user_input, context):
# 第一层:关键词匹配(快路径)
keyword_intent = match_keywords(user_input)
if keyword_intent and keyword_intent.confidence > 0.9:
return keyword_intent # 高置信度,直接返回
# 第二层:语义理解(慢路径)
semantic_intent = llm_understand_intent(user_input, context)
if semantic_intent.confidence > 0.7:
# 记录新模式,丰富规则库
pattern_db.record(user_input, semantic_intent)
return semantic_intent
# 第三层:模式识别(兜底)
pattern_intent = match_patterns(user_input, pattern_db)
return pattern_intent or semantic_intent
# 示例
print(identify_intent("明天天气咋样", []))
# → Intent(type="查询", sub_type="天气", confidence=0.95) # 关键词命中
print(identify_intent("我项目里这段代码跑不通,帮我看看", []))
# → Intent(type="代码修改", confidence=0.85) # 语义理解命中
// 分层意图识别:关键词 → 语义 → 模式
function identifyIntent(userInput: string, context: Context[]): Intent {
// 第一层:关键词匹配(快路径)
const keywordIntent = matchKeywords(userInput);
if (keywordIntent && keywordIntent.confidence > 0.9) {
return keywordIntent;
}
// 第二层:语义理解(慢路径)
const semanticIntent = llmUnderstandIntent(userInput, context);
if (semanticIntent.confidence > 0.7) {
patternDB.record(userInput, semanticIntent);
return semanticIntent;
}
// 第三层:模式识别(兜底)
const patternIntent = matchPatterns(userInput, patternDB);
return patternIntent || semanticIntent;
}
// 分层意图识别:关键词 → 语义 → 模式
func IdentifyIntent(userInput string, context []Context) Intent {
// 第一层:关键词匹配(快路径)
keywordIntent := matchKeywords(userInput)
if keywordIntent.Confidence > 0.9 {
return keywordIntent
}
// 第二层:语义理解(慢路径)
semanticIntent := llmUnderstandIntent(userInput, context)
if semanticIntent.Confidence > 0.7 {
patternDB.Record(userInput, semanticIntent)
return semanticIntent
}
// 第三层:模式识别(兜底)
patternIntent := matchPatterns(userInput, patternDB)
if patternIntent.Confidence > 0 {
return patternIntent
}
return semanticIntent
}
// 分层意图识别:关键词 → 语义 → 模式
public class IntentIdentifier {
public Intent identifyIntent(String userInput, List context) {
// 第一层:关键词匹配(快路径)
Intent keywordIntent = matchKeywords(userInput);
if (keywordIntent != null && keywordIntent.getConfidence() > 0.9) {
return keywordIntent;
}
// 第二层:语义理解(慢路径)
Intent semanticIntent = llmUnderstandIntent(userInput, context);
if (semanticIntent.getConfidence() > 0.7) {
patternDB.record(userInput, semanticIntent);
return semanticIntent;
}
// 第三层:模式识别(兜底)
Intent patternIntent = matchPatterns(userInput, patternDB);
return patternIntent != null ? patternIntent : semanticIntent;
}
}
隐式意图识别
用户不会总是直接说"我要查天气"。很多时候意图隐藏在自然表达中:
隐式意图识别示例 | 用户说 | 显式意图 | 隐式意图 | Agent 应该做 | | --- | --- | --- | --- | | "外面冷不冷" | 无(没说"查天气") | 查询天气 | 调天气 API + 给穿衣建议 | | "明天适合跑步吗" | 无 | 天气+空气质量+温度 | 多工具协作查询 | | "这段代码有 bug" | 无(没说"修 bug") | 代码分析+修复 | 读取代码 → 定位 → 修复 | 意图识别的核心不是关键词匹配,而是理解用户想达成的目标。"查天气"和"外面冷不冷"表面完全不同,但意图相同。这就需要 LLM 的语义理解能力——这也是为什么 Agent 不用规则引擎,而用 LLM 作为大脑。
复合意图拆解
用户一句话可能包含多个意图:
复合意图拆解
用户:"帮我查下北京明天天气,顺便订个闹钟提醒我带伞"
意图拆解:
① 查询意图 → 调用天气 API → 获取北京明天天气
② 判断意图 → 根据天气判断是否需要带伞
③ 执行意图 → 调用闹钟 API → 设置明早提醒
Agent 执行计划:
Step 1: call weather_api(city="北京", date="明天")
Step 2: if rain_probability > 0.3 → call alarm_api(msg="带伞")
Step 3: return "北京明天小雨,已设闹钟提醒带伞"
复合意图拆解需要 LLM 先理解整句话的语义,再拆分出子意图,最后生成执行计划。这个过程就是任务规划——我们在 §6.4 会详细讲。
7.2 工具路由:从意图到工具
意图识别完成后,Agent 需要把意图路由到正确的工具。这就是工具路由——决策中枢的"分发器"。
意图分类体系
工具路由的第一步是建立意图分类体系。不同类型的意图对应不同的工具集合:
意图分类 → 工具路由映射 | 意图大类 | 子意图 | 路由目标 | 示例 | | --- | --- | --- | --- | | 查询类 | 天气查询 | weather_api | "明天北京天气" | | 知识查询 | rag_search / web_search | "什么是 Transformer" | | 数据查询 | db_query / sql_executor | "上个月销售额多少" | | 执行类 | 代码执行 | code_runner / sandbox | "帮我跑下这段 Python" | | 文件操作 | file_handler | "把这个文件重命名" | | 分析类 | 趋势分析 | data_analyzer + chart_generator | "分析房价趋势" | | 代码审查 | code_analyzer + lint_tool | "帮我 review 这段代码" | ### Tool RAG:工具检索策略
当 Agent 注册了几十个甚至上百个工具时,把所有工具描述都塞进 Prompt 会导致 Token 爆炸。解决方案是 Tool RAG——根据用户意图,只检索最相关的工具描述注入 Prompt。
全量注入 vs JIT 加载 vs Tool RAG | 策略 | 原理 | Token 消耗 | 准确率 | 适用规模 | | --- | --- | --- | --- | --- | | 全量注入 | 所有工具描述都放入 Prompt | 极高 | 最高 | ≤10 个工具 | | JIT 加载 | 意图识别后只加载匹配工具 | 低 | 中等(依赖意图识别) | 10-50 个工具 | | Tool RAG | 向量检索最相关的 K 个工具 | 可控 | 高 | 50+ 个工具 | ```
Tool RAG:向量检索最相关的工具
class ToolRAG: def init(self, tool_registry, embed_model): self.tools = tool_registry # {name: description} self.embed_model = embed_model
预计算所有工具描述的 embedding
self.tool_embeddings = { name: self.embed_model.embed(desc) for name, desc in self.tools.items() }
def retrieve(self, user_input, top_k=3): """检索与用户输入最相关的 top_k 个工具""" query_emb = self.embed_model.embed(user_input)
scores = [] for name, tool_emb in self.tool_embeddings.items(): sim = cosine_similarity(query_emb, tool_emb) scores.append((name, sim))
取相似度最高的 K 个
scores.sort(key=lambda x: x[1], reverse=True) return scores[:top_k]
def build_tool_prompt(self, user_input, top_k=3): """构建只包含相关工具的 Prompt""" relevant = self.retrieve(user_input, top_k) tools_desc = "\n".join( f"- {name}: {self.tools[name]}" for name, _ in relevant ) return f"可用工具:\n{tools_desc}"
示例
rag = ToolRAG(tool_registry, embed_model) print(rag.build_tool_prompt("明天北京天气怎么样"))
可用工具:
- weather_api: 查询指定城市的天气
- clothing_advisor: 根据天气推荐穿搭
- alarm_api: 设置闹钟提醒
// Tool RAG:向量检索最相关的工具
class ToolRAG { private toolEmbeddings: Map;
constructor(tools: Map, embedModel: EmbedModel) { this.toolEmbeddings = new Map(); for (const [name, desc] of tools) { this.toolEmbeddings.set(name, embedModel.embed(desc)); } }
retrieve(userInput: string, topK = 3): [string, number][] { const queryEmb = embedModel.embed(userInput); const scores: [string, number][] = []; for (const [name, emb] of this.toolEmbeddings) { scores.push([name, cosineSimilarity(queryEmb, emb)]); } return scores.sort((a, b) => b[1] - a[1]).slice(0, topK); }
buildToolPrompt(userInput: string, topK = 3): string {
const relevant = this.retrieve(userInput, topK);
const desc = relevant.map(([name]) => - ${name}: ${tools.get(name)}).join('\n');
return 可用工具:\n${desc};
}
}
// Tool RAG:向量检索最相关的工具
type ToolRAG struct { tools map[string]string embeddings map[string][]float64 embedModel EmbedModel }
func NewToolRAG(tools map[string]string, embedModel EmbedModel) *ToolRAG { embeddings := make(map[string][]float64) for name, desc := range tools { embeddings[name] = embedModel.Embed(desc) } return &ToolRAG{tools: tools, embeddings: embeddings, embedModel: embedModel} }
func (r *ToolRAG) Retrieve(userInput string, topK int) []ToolScore { queryEmb := r.embedModel.Embed(userInput) var scores []ToolScore for name, emb := range r.embeddings { scores = append(scores, ToolScore{Name: name, Score: cosineSimilarity(queryEmb, emb)}) } sort.Slice(scores, func(i, j int) bool { return scores[i].Score > scores[j].Score }) if topK > len(scores) { topK = len(scores) } return scores[:topK] }
// Tool RAG:向量检索最相关的工具
public class ToolRAG { private final Map tools; private final Map embeddings; private final EmbedModel embedModel;
public ToolRAG(Map tools, EmbedModel embedModel) { this.tools = tools; this.embedModel = embedModel; this.embeddings = new HashMap<>(); for (var entry : tools.entrySet()) { embeddings.put(entry.getKey(), embedModel.embed(entry.getValue())); } }
public List retrieve(String userInput, int topK) { float[] queryEmb = embedModel.embed(userInput); List scores = new ArrayList<>(); for (var entry : embeddings.entrySet()) { scores.add(new ToolScore(entry.getKey(), cosineSimilarity(queryEmb, entry.getValue()))); } scores.sort((a, b) -> Float.compare(b.score, a.score)); return scores.subList(0, Math.min(topK, scores.size())); } }
### 工具描述优化:高信号低噪声
工具描述的质量直接影响路由准确率。好的描述应该**高信号、低噪声**——让 LLM 一眼看出工具能做什么、什么时候该用。
工具描述优化前后对比
**❌ 差的描述:**
{ "name": "api_call", "description": "调用 API" }
**✅ 好的描述:**
{ "name": "weather_api", "description": "查询指定城市的实时天气和未来预报。支持全球城市。当用户询问天气、温度、降雨、穿衣建议时使用。", "parameters": { "city": {"type": "string", "description": "城市名称,如'北京'、'上海'"}, "date": {"type": "string", "description": "日期,'今天'、'明天'或具体日期"} } }
## 7.3 任务分类与复杂度判断
意图识别之后,Agent 需要对任务进行**分类**,决定**执行策略**。不同复杂度的任务对应不同的处理路径。这是决策中枢的"调度器"。
为什么需要任务分类?因为"用牛刀杀鸡"和"用鸡刀杀牛"都是浪费。一个简单的事实问答(如"今天几号"),如果走完整的多 Agent 规划+执行流程,会多花 5-10 倍的 Token 和延迟;而一个复杂的多步骤任务(如"分析这份财报并生成投资建议"),如果当作简单问答一次 LLM 调用就回答,结果必然是浅薄甚至错误的。任务分类的本质,是为不同难度的任务**匹配恰好够用的计算资源**——这正是"成本-质量"平衡在决策层的体现。
### 任务分类矩阵
不同的任务类型对应不同的执行路径:
任务分类矩阵:类型 → 策略 → 路径 | 任务类型 | 复杂度 | 执行路径 | 示例 | | --- | --- | --- | --- | | 简单问答 | Low | direct_answer(纯模型推理) | "什么是 Transformer" | | 代码解释 | Medium | analyze_explain(读取+分析) | "这段代码什么意思" | | 代码修改 | Medium-High | locate_modify(定位+修改+验证) | "帮我修复这个 bug" | | 复杂开发 | High | decompose_execute(拆解+多步执行) | "帮我搭建一个 REST API" | ```
# 任务分类器:判断复杂度并选择执行路径
class TaskClassifier:
COMPLEXITY_RULES = {
"简单问答": {
"keywords": ["是什么", "怎么用", "区别", "概念"],
"path": "direct_answer",
"complexity": "low"
},
"代码解释": {
"keywords": ["这段代码", "什么意思", "解释一下"],
"path": "analyze_explain",
"complexity": "medium"
},
"代码修改": {
"keywords": ["修改", "改成", "优化", "修复", "重构"],
"path": "locate_modify",
"complexity": "medium_high"
},
"复杂开发": {
"keywords": ["搭建", "实现", "开发", "创建项目", "从零开始"],
"path": "decompose_execute",
"complexity": "high"
}
}
def classify(self, intent):
for task_type, rule in self.COMPLEXITY_RULES.items():
if any(kw in intent.raw_input for kw in rule["keywords"]):
return task_type, rule["complexity"], rule["path"]
# 规则未命中,交给 LLM 判断
return self.llm_classify(intent)
// 任务分类器
const COMPLEXITY_RULES: Record = {
'简单问答': { keywords: ['是什么', '怎么用', '区别', '概念'], path: 'direct_answer', complexity: 'low' },
'代码解释': { keywords: ['这段代码', '什么意思', '解释一下'], path: 'analyze_explain', complexity: 'medium' },
'代码修改': { keywords: ['修改', '改成', '优化', '修复', '重构'], path: 'locate_modify', complexity: 'medium_high' },
'复杂开发': { keywords: ['搭建', '实现', '开发', '创建项目', '从零开始'], path: 'decompose_execute', complexity: 'high' }
};
function classify(intent: Intent): ClassifyResult {
for (const [taskType, rule] of Object.entries(COMPLEXITY_RULES)) {
if (rule.keywords.some(kw => intent.rawInput.includes(kw))) {
return { taskType, complexity: rule.complexity, path: rule.path };
}
}
return llmClassify(intent);
}
// 任务分类器
type ComplexityRule struct {
Keywords []string
Path string
Complexity string
}
type TaskClassifier struct {
rules map[string]ComplexityRule
}
func (c *TaskClassifier) Classify(intent Intent) (taskType, complexity, path string) {
for name, rule := range c.rules {
for _, kw := range rule.Keywords {
if strings.Contains(intent.RawInput, kw) {
return name, rule.Complexity, rule.Path
}
}
}
return llmClassify(intent)
}
// 任务分类器
public class TaskClassifier {
private static final Map RULES = new HashMap<>();
static {
RULES.put("简单问答", new ComplexityRule(List.of("是什么","怎么用","区别","概念"), "direct_answer", "low"));
RULES.put("代码解释", new ComplexityRule(List.of("这段代码","什么意思","解释一下"), "analyze_explain", "medium"));
RULES.put("代码修改", new ComplexityRule(List.of("修改","改成","优化","修复","重构"), "locate_modify", "medium_high"));
RULES.put("复杂开发", new ComplexityRule(List.of("搭建","实现","开发","创建项目","从零开始"), "decompose_execute", "high"));
}
public ClassifyResult classify(Intent intent) {
for (var entry : RULES.entrySet()) {
for (String kw : entry.getValue().keywords) {
if (intent.getRawInput().contains(kw)) {
return new ClassifyResult(entry.getKey(), entry.getValue().complexity, entry.getValue().path);
}
}
}
return llmClassify(intent);
}
}
分类决策流程图
7.4 任务规划与分解
当任务复杂度被判定为"High"时,Agent 需要将大任务拆解为子任务,生成执行计划。这就是任务规划——决策中枢的"项目管理器"。
Plan & Execute 范式
ReAct 模式是"边想边做",每一步都调 LLM 推理。而 Plan & Execute 是"先想好再做"——先让 LLM 生成完整执行计划,再逐步执行。两种范式各有优劣:
ReAct vs Plan & Execute | 维度 | ReAct(边想边做) | Plan & Execute(先想后做) | | --- | --- | --- | | 执行方式 | Thought → Action → Observation 循环 | Planner 生成计划 → Executor 逐步执行 | | LLM 调用 | 每步都调 LLM | 规划时调一次,执行时可不调 | | Token 消耗 | 高(N 步 = N 次推理) | 低(1 次规划 + N 次执行) | | 灵活性 | 高(每步可调整) | 中(计划生成后不易调整) | | 适用场景 | 探索性任务、不确定性高 | 明确目标、步骤可预测 | ```
Plan & Execute:先规划后执行
class TaskPlanner: def init(self, llm): self.llm = llm
def decompose(self, task, available_tools): """将复杂任务拆解为子任务列表""" prompt = f""" 任务: {task} 可用工具: {available_tools}
请将任务拆解为具体的子任务步骤,每个步骤包含:
- step_id: 步骤编号
- action: 具体操作
- tool: 使用的工具名称
- depends_on: 依赖的前置步骤 """ plan = self.llm.generate(prompt) return self._parse_plan(plan)
def _parse_plan(self, plan_text): """解析 LLM 输出的执行计划""" steps = [] for line in plan_text.strip().split('\n'): if line.strip().startswith('Step'): steps.append(self._parse_step(line)) return steps
示例
planner = TaskPlanner(LLMEngine()) plan = planner.decompose( "帮我分析北京最近房价趋势并生成报告", ["search_api", "data_analyzer", "chart_generator", "file_writer"] )
输出:
[
{step_id: 1, action: "搜索北京房价数据", tool: "search_api", depends_on: []},
{step_id: 2, action: "分析趋势", tool: "data_analyzer", depends_on: [1]},
{step_id: 3, action: "生成图表", tool: "chart_generator", depends_on: [2]},
{step_id: 4, action: "写入报告文件", tool: "file_writer", depends_on: [3]}
]
// Plan & Execute:先规划后执行
class TaskPlanner { constructor(private llm: LLMEngine) {}
async decompose(task: string, tools: string[]): Promise {
const prompt = 任务: ${task}\n可用工具: ${tools.join(', ')}\n请拆解为子任务步骤。;
const result = await this.llm.generate(prompt);
return this.parsePlan(result);
}
private parsePlan(text: string): PlanStep[] { return text.trim().split('\n') .filter(line => line.startsWith('Step')) .map(line => this.parseStep(line)); } }
// Plan & Execute:先规划后执行
type TaskPlanner struct { llm *LLMEngine }
func (p *TaskPlanner) Decompose(task string, tools []string) ([]PlanStep, error) { prompt := fmt.Sprintf("任务: %s\n可用工具: %v\n请拆解为子任务步骤。", task, tools) result, err := p.llm.Generate(prompt) if err != nil { return nil, err } return parsePlan(result), nil }
// Plan & Execute:先规划后执行
public class TaskPlanner { private final LLMEngine llm;
public List decompose(String task, List tools) { String prompt = String.format("任务: %s%n可用工具: %s%n请拆解为子任务步骤。", task, tools); String result = llm.generate(prompt); return parsePlan(result); } }
### 调度器:执行计划的核心
计划生成后,调度器负责按计划执行。调度器是 Agent 决策中枢的"执行引擎",它根据子任务特征,决定调用模型还是工具:
调度器:根据任务特征分配执行路径
class Scheduler: def init(self, llm, tools): self.llm = llm self.tools = tools
def dispatch(self, sub_task): """根据子任务特征,决定调用模型还是工具"""
判断1:是否需要外部数据?
if sub_task.requires_external_data: tool_name = self._select_tool(sub_task) result = self.tools.call(tool_name, sub_task.params) return result
判断2:是否需要逻辑推理?
if sub_task.requires_reasoning: return self.llm.reason(sub_task.prompt)
判断3:是否需要格式化输出?
if sub_task.requires_formatting: return self.llm.format(sub_task.raw_output)
判断4:是否需要验证结果?
if sub_task.requires_verification: tool_result = self.tools.call("verify", sub_task.params) return self.llm.reason(f"验证以下结果是否合理: {tool_result}")
默认:模型处理
return self.llm.reason(sub_task.prompt)
def _select_tool(self, sub_task): tool_map = { "search": "search_api", "database": "db_query", "code_execution": "code_runner", "file_operation": "file_handler", "calculation": "calculator" } return tool_map.get(sub_task.tool_type, "general_tool")
// 调度器
class Scheduler { constructor(private llm: LLMEngine, private tools: ToolRegistry) {}
dispatch(subTask: SubTask): any {
if (subTask.requiresExternalData) {
const toolName = this.selectTool(subTask);
return this.tools.call(toolName, subTask.params);
}
if (subTask.requiresReasoning) {
return this.llm.reason(subTask.prompt);
}
if (subTask.requiresFormatting) {
return this.llm.format(subTask.rawOutput);
}
if (subTask.requiresVerification) {
const toolResult = this.tools.call('verify', subTask.params);
return this.llm.reason(验证: ${toolResult});
}
return this.llm.reason(subTask.prompt);
}
private selectTool(subTask: SubTask): string { const map: Record = { search: 'search_api', database: 'db_query', code_execution: 'code_runner', file_operation: 'file_handler' }; return map[subTask.toolType] || 'general_tool'; } }
// 调度器
type Scheduler struct { llm *LLMEngine tools *ToolRegistry }
func (s *Scheduler) Dispatch(subTask *SubTask) interface{} { if subTask.RequiresExternalData { toolName := s.selectTool(subTask) return s.tools.Call(toolName, subTask.Params) } if subTask.RequiresReasoning { return s.llm.Reason(subTask.Prompt) } if subTask.RequiresFormatting { return s.llm.Format(subTask.RawOutput) } if subTask.RequiresVerification { toolResult := s.tools.Call("verify", subTask.Params) return s.llm.Reason(fmt.Sprintf("验证: %v", toolResult)) } return s.llm.Reason(subTask.Prompt) }
// 调度器
public class Scheduler { private final LLMEngine llm; private final ToolRegistry tools;
public Object dispatch(SubTask subTask) { if (subTask.requiresExternalData) { String toolName = selectTool(subTask); return tools.call(toolName, subTask.params); } if (subTask.requiresReasoning) { return llm.reason(subTask.prompt); } if (subTask.requiresFormatting) { return llm.format(subTask.rawOutput); } if (subTask.requiresVerification) { Object toolResult = tools.call("verify", subTask.params); return llm.reason("验证: " + toolResult); } return llm.reason(subTask.prompt); } }
## 7.5 上下文工程:大脑的工作记忆管理
Agent 在执行过程中会产生大量上下文——对话历史、工具返回结果、中间推理。如果全部塞进 Prompt,Token 会爆炸;如果裁剪太多,Agent 会"失忆"。这就是**上下文工程**要解决的问题。
"上下文工程(Context Engineering)是提示工程(Prompt Engineering)的升级版。提示工程关注'怎么问',上下文工程关注'在问的时候给模型看什么'。"
### JIT Context:在正确的时间提供正确的信息
**Just-in-Time Context**(即时上下文)的核心思想:不要一开始就把所有信息塞进 Prompt,而是在需要的时候才加载。这就像人不需要记住所有知识,只需要知道"去哪里查"。
JIT Context 加载策略 | 信息类型 | 加载时机 | 加载方式 | 示例 | | --- | --- | --- | --- | | System Prompt | 会话开始时 | 全量注入 | 角色设定、行为规则 | | 工具描述 | 意图识别后 | Tool RAG 检索 | 只加载相关工具 | | 长期记忆 | 需要时检索 | 向量检索 top-K | 用户偏好、历史决策 | | 工具结果 | 调用后立即 | 压缩后注入 | API 返回数据 | ### 工具结果压缩策略
工具返回的数据往往很大(一个 API 可能返回几 KB 甚至几十 KB),直接塞进 Prompt 会浪费大量 Token。需要**压缩工具结果**,保留关键信息,丢弃冗余。
多策略工具结果压缩器
class ToolResultCompressor: """压缩工具返回结果:保留关键信息,压缩冗余"""
STRATEGIES = { "selective": "选择性保留关键字段", "truncation": "截断超长结果", "summary": "AI 摘要核心信息", "structured": "提取结构化数据" }
def compress(self, result, strategy="selective", max_tokens=500): if strategy == "selective": return self._selective_compress(result, max_tokens) elif strategy == "summary": return self._ai_summary(result, max_tokens) elif strategy == "truncation": return self._truncate(result, max_tokens) return result
def _selective_compress(self, result, max_tokens): """选择性保留:只保留关键字段""" if isinstance(result, dict): key_fields = ["status", "data", "error", "summary"] compressed = {k: result[k] for k in key_fields if k in result} compressed["_meta"] = { "compressed": True, "original_keys": list(result.keys()), "compressed_tokens": self._estimate_tokens(compressed) } return compressed return {"value": str(result)[:max_tokens * 4]}
def _ai_summary(self, result, max_tokens): """AI 摘要:让 LLM 总结工具结果""" summary = llm.summarize(str(result), max_tokens=max_tokens) return {"_summary": summary, "_meta": {"strategy": "ai_summary"}}
def _truncate(self, result, max_tokens): """截断:直接截取前 N 个字符""" text = str(result) return {"value": text[:max_tokens * 4], "_meta": {"truncated": True}}
def _estimate_tokens(self, obj): return len(str(obj)) // 4 # 粗略估算
示例
compressor = ToolResultCompressor() raw = {"status": "ok", "data": [1,2,3]*100, "debug": "x"*2000, "cache": "y"*1000} compressed = compressor.compress(raw, strategy="selective")
压缩后只保留3个关键字段 ≈ 150 tokens
print(f"压缩: {compressed['_meta']['compressed_tokens']} tokens")
// 多策略工具结果压缩器
class ToolResultCompressor { compress(result: any, strategy = 'selective', maxTokens = 500): any { switch (strategy) { case 'selective': return this.selectiveCompress(result, maxTokens); case 'summary': return this.aiSummary(result, maxTokens); case 'truncation': return this.truncate(result, maxTokens); default: return result; } }
private selectiveCompress(result: any, maxTokens: number): any { if (typeof result === 'object') { const keyFields = ['status', 'data', 'error', 'summary']; const compressed: any = {}; for (const k of keyFields) if (k in result) compressed[k] = result[k]; compressed._meta = { compressed: true, originalKeys: Object.keys(result) }; return compressed; } return { value: String(result).slice(0, maxTokens * 4) }; }
private truncate(result: any, maxTokens: number): any { return { value: String(result).slice(0, maxTokens * 4), _meta: { truncated: true } }; } }
// 多策略工具结果压缩器
type ToolResultCompressor struct{}
func (c ToolResultCompressor) Compress(result map[string]interface{}, strategy string, maxTokens int) map[string]interface{} { switch strategy { case "selective": return c.selectiveCompress(result, maxTokens) case "truncation": text := fmt.Sprintf("%v", result) return map[string]interface{}{"value": text[:maxTokens4], "_meta": map[string]bool{"truncated": true}} default: return result } }
func (c *ToolResultCompressor) selectiveCompress(result map[string]interface{}, maxTokens int) map[string]interface{} { keyFields := []string{"status", "data", "error", "summary"} compressed := make(map[string]interface{}) for _, k := range keyFields { if v, ok := result[k]; ok { compressed[k] = v } } compressed["_meta"] = map[string]interface{}{"compressed": true} return compressed }
// 多策略工具结果压缩器
public class ToolResultCompressor {
public Map compress(Map result, String strategy, int maxTokens) { switch (strategy) { case "selective": return selectiveCompress(result); case "truncation": String text = result.toString(); return Map.of("value", text.substring(0, Math.min(text.length(), maxTokens * 4)), "_meta", Map.of("truncated", true)); default: return result; } }
private Map selectiveCompress(Map result) { List keyFields = List.of("status", "data", "error", "summary"); Map compressed = new HashMap<>(); for (String k : keyFields) { if (result.containsKey(k)) compressed.put(k, result.get(k)); } compressed.put("_meta", Map.of("compressed", true)); return compressed; } }
### Token 消耗对比:全量 vs JIT vs 路由
三种上下文管理策略的 Token 消耗对比 | 策略 | System Prompt | 工具描述 | 对话历史 | 工具结果 | 总 Token | | --- | --- | --- | --- | --- | --- | | 全量注入 | 500 | 2000(50个工具) | 3000(全量) | 2000(原始) | 7500 | | JIT 加载 | 500 | 200(3个工具) | 1500(压缩) | 500(压缩) | 2700 | | Tool RAG + 压缩 | 500 | 150(向量检索3个) | 1000(增量压缩) | 300(选择性保留) | 1950 | Tool RAG + 压缩策略比全量注入节省 **74%** 的 Token 消耗。
## 7.6 Agent 执行循环引擎
意图识别、任务规划、工具路由——这些大脑组件需要一个**执行引擎**把它们串起来循环运行。这就是 Agent Loop。第5章讲了 ReAct 循环的原理(Thought → Action → Observation),本节聚焦**生产级执行循环的工程实现**。
### 生产级 Agent 主循环设计
生产环境的 Agent 主循环不是简单的 while True,需要处理:双模式切换(Chat vs Agent)、Token Budget 管理、死循环保护、max_tokens 截断恢复、指数退避重试、空闲超时检测。
生产级 Agent 主循环
class AgentLoop: def init(self, llm, tools, memory, config=None): self.llm = llm self.tools = tools self.memory = memory self.config = config or { "max_turns": 20, "max_tokens": 8000, "max_tool_errors": 3, "idle_timeout": 300, # 5分钟 "retry_backoff": [1, 2, 4, 8, 16] } self._pending_timers = []
def run(self, user_input): """Agent 主循环:感知→推理→行动→观察→再推理""" messages = self.memory.build_messages(user_input) turn_count = 0 total_tokens = 0 tool_errors = 0
while turn_count = self.config["max_tool_errors"]: return f"工具连续失败 {tool_errors} 次,终止执行。最后错误: {e}" messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": f"Error: {e}"})
Step 4: 上下文压缩检查
if total_tokens > self.config["max_tokens"] * 0.8: messages = self.memory.compress(messages) total_tokens = self._estimate_tokens(messages)
return f"达到最大轮次 ({self.config['max_turns']}),任务未完成。"
def _safe_tool_call(self, tool_call): """安全工具调用:带超时和异常处理""" return self.tools.call(tool_call.name, tool_call.arguments)
def _compress_result(self, result): """压缩工具结果""" return ToolResultCompressor().compress(result, strategy="selective")
// 生产级 Agent 主循环
class AgentLoop { private config = { maxTurns: 20, maxTokens: 8000, maxToolErrors: 3, idleTimeout: 300, retryBackoff: [1, 2, 4, 8, 16] };
async run(userInput: string): Promise { let messages = this.memory.buildMessages(userInput); let turnCount = 0; let totalTokens = 0; let toolErrors = 0;
while (turnCount = this.config.maxToolErrors) {
return 工具连续失败 ${toolErrors} 次,终止。;
}
messages.push({ role: 'tool', tool_call_id: call.id, content: Error: ${e} });
}
}
if (totalTokens > this.config.maxTokens * 0.8) {
messages = this.memory.compress(messages);
}
} catch (e) {
const backoff = this.config.retryBackoff[Math.min(toolErrors, 4)];
await new Promise(r => setTimeout(r, backoff * 1000));
toolErrors++;
}
}
return 达到最大轮次 (${this.config.maxTurns}),任务未完成。;
}
}
// 生产级 Agent 主循环
type AgentLoop struct { llm *LLMEngine tools *ToolRegistry memory *MemoryStore config LoopConfig }
func (loop *AgentLoop) Run(userInput string) (string, error) { messages := loop.memory.BuildMessages(userInput) turnCount := 0 totalTokens := 0 toolErrors := 0
for turnCount = loop.config.MaxToolErrors {
return "", fmt.Errorf("工具连续失败 %d 次", toolErrors) } } messages = append(messages, Message{Role: "tool", Content: compressResult(result)}) }
if totalTokens > loop.config.MaxTokens*4/5 { messages = loop.memory.Compress(messages) } } return "", fmt.Errorf("达到最大轮次 %d", loop.config.MaxTurns) }
// 生产级 Agent 主循环
public class AgentLoop { private final LLMEngine llm; private final ToolRegistry tools; private final MemoryStore memory; private final LoopConfig config = new LoopConfig();
public String run(String userInput) { List messages = memory.buildMessages(userInput); int turnCount = 0, totalTokens = 0, toolErrors = 0;
while (turnCount = config.maxToolErrors) { return "工具连续失败 " + toolErrors + " 次,终止。"; } messages.add(new Message("tool", "Error: " + e.getMessage())); } }
if (totalTokens > config.maxTokens * 0.8) { messages = memory.compress(messages); } } catch (Exception e) { int backoff = config.retryBackoff[Math.min(toolErrors, 4)]; try { Thread.sleep(backoff * 1000L); } catch (InterruptedException ignored) {} toolErrors++; } } return "达到最大轮次 (" + config.maxTurns + "),任务未完成。"; } }
### 死循环保护:递减收益检测
Agent 有时会陷入"无效循环"——反复调用同一个工具、产出相同结果。需要**递减收益检测**(Diminishing Returns Detection)来打破死循环。
死循环检测策略 | 检测策略 | 原理 | 阈值 | | --- | --- | --- | | 重复工具检测 | 连续调用同一工具且参数相同 | ≥ 3 次 | | 相似输出检测 | 连续 N 轮输出语义相似度 > 0.9 | ≥ 3 轮 | | 无进展检测 | 轮次间上下文无新增有效信息 | ≥ 5 轮 | ## 7.7 单一 Agent vs 多 Agent:执行方式选型
Agent 的执行方式不只是"一个 Agent 干到底"。当任务足够复杂时,需要**多个 Agent 协作**。决策中枢需要判断:这个任务应该一个 Agent 做,还是拆给多个 Agent?
### 单 Agent 的瓶颈
单 Agent 的核心问题是**上下文爆炸**和**能力冲突**:
单 Agent 的三大瓶颈
- **上下文爆炸**:任务越复杂,需要的工具/记忆/中间结果越多,Prompt 膨胀导致 Token 超限
- **能力冲突**:一个 Agent 同时扮演"搜索者"和"写作者",System Prompt 冲突,角色混乱
- **串行瓶颈**:所有步骤串行执行,无法并行处理独立子任务
### 何时用多 Agent?决策矩阵
单 Agent vs 多 Agent 决策矩阵 | 维度 | 单 Agent | 多 Agent | | --- | --- | --- | | 任务步数 | ≤ 5 步 | > 5 步或有并行子任务 | | 工具数量 | ≤ 10 个 | > 10 个,可分组 | | 角色需求 | 单一角色 | 多角色(搜索者+写作者+审核者) | | 上下文 | 可放入一个 Prompt | 超出单 Prompt 容量 | | 延迟要求 | 串行可接受 | 需要并行加速 | 多 Agent 的详细协作模式(串行流水线、编排者-执行者、辩论对抗、自主协作)和通信机制,详见 **第14章 多 Agent 系统架构**。
**📋 八股总结 — 面试高频考点**
Q1: Agent 的意图识别有哪些方法?生产环境怎么选?
三种方法:关键词匹配(快、便宜但粗糙)、语义理解(准、但贵)、模式识别(越用越准但冷启动差)。
生产环境用**分层组合**:先关键词快速筛选 → 命中则直接路由;未命中走 LLM 语义理解;结果记录到模式库优化规则。这样兼顾速度和准确率,90% 的常见意图走快路径,只有 10% 走昂贵的 LLM 路径。
Q2: Tool RAG 是什么?和普通 RAG 有什么区别?
Tool RAG 是对工具描述做向量检索,只把最相关的 K 个工具描述注入 Prompt。
普通 RAG 检索的是**知识文档**,Tool RAG 检索的是**工具 Schema**。当工具数量 > 20 个时,Tool RAG 比全量注入节省 70%+ Token,同时减少工具选择混淆(工具太多 LLM 反而选不对)。
Q3: ReAct 和 Plan & Execute 有什么区别?什么时候用哪个?
**ReAct** 是"边想边做"(Thought→Action→Observation 循环),每步调 LLM,灵活但 Token 消耗高。
**Plan & Execute** 是"先想后做"(Planner 生成完整计划 → Executor 逐步执行),Token 效率高但灵活性低。
**选型原则**:探索性任务用 ReAct,明确目标用 Plan & Execute,复杂任务可混合——先 Plan 再用 ReAct 执行每个子任务。
Q4: Agent 主循环需要哪些保护机制?
五大保护:
①**max_turns** 防止无限循环;
②**Token Budget** 防止上下文爆炸(达 80% 阈值触发压缩);
③**递减收益检测** 防止死循环(连续 3 轮输出相似度 > 0.9 则终止);
④**指数退避重试** 处理瞬时故障(1s→2s→4s→8s→16s);
⑤**idle_timeout** 防止资源占用(5 分钟无进展自动释放)。
Q5: 什么时候该用多 Agent 而不是单 Agent?
三个信号:
①任务步数 > 5 步且有并行子任务;
②需要多种角色(搜索者+写作者+审核者),System Prompt 冲突;
③工具数量 > 10 个,上下文超限。
多 Agent 的核心优势是**上下文隔离**(每个 Agent 只看自己需要的信息)和**并行执行**(独立子任务同时跑)。
Q6: 上下文工程(Context Engineering)和提示工程(Prompt Engineering)有什么区别?
提示工程关注"怎么问"——System Prompt 怎么写、Few-shot 怎么选。
上下文工程关注"在问的时候给模型看什么"——工具描述怎么加载(JIT/Tool RAG)、对话历史怎么压缩、工具结果怎么裁剪、长期记忆怎么检索。
上下文工程是提示工程的**超集**,它涵盖了所有进入 LLM 上下文窗口的信息管理。
Q7: Agent 决策中枢的完整链路是什么?
七步链路:
①**感知**(接收输入)→ ②**意图识别**(理解用户目标)→ ③**任务分类**(判断复杂度)→ ④**任务规划**(拆解子任务)→ ⑤**工具路由**(选工具)→ ⑥**执行调度**(调模型/工具)→ ⑦**上下文管理**(压缩/记忆)。
这七步循环运行,就是 Agent Loop。每一轮的上下文管理结果会反馈到下一轮的感知阶段,形成闭环。