7987 字
约 26 分钟
1
第7章 意图识别与决策中枢

第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。每一轮的上下文管理结果会反馈到下一轮的感知阶段,形成闭环。
第7章 意图识别与决策中枢
http://www.clxhxhhr.top/posts/710/
作者
clxstart
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。