2026 前端 AI Agent 工程化实战营系列第 11 篇

本文整理自《2026 前端 AI Agent 工程化实战营》课程资料,作为系列文章第 11 篇。


theme: channing-cyan

generated-image-1778414786997.png

本章demo地址feat/token

前九章把 Agent 系统从模型调用一路推进到了生产级 Multi-Agent 流水线。到这里,系统已经能跑起来;但还有一个问题一直没有正面展开:这套系统每跑一次,要花多少钱?

每个 Agent 每次调用模型都会消耗 Token。第九章的 Supervisor + 4 个专家子图 + Critic-Refine 循环,单次需求分析可能调用模型 9–12 次;每次调用都要把系统提示词、工具定义、澄清后的需求和中间结果重新提交给模型——这些内容都会计入 Token,也都会进入成本。

本章从 Token 的基础原理开始,逐步展开成本拆账、上下文膨胀分析、6 种省 Token 策略、节点级采集与持久化、预算控制与自动降级。目标不是单纯“省钱”,而是让 Multi-Agent 系统在能力、质量和成本之间更可控。

本章验收点


10.1 Token 不是概念,是 Agent 的运行成本

10.1.1 什么是 Token

generated-image-1778414792247.png

Token 不是"字",也不是"词"。它是模型词表中的最小单元。不同模型使用不同的 Tokenizer(分词器),但基本原理相同:把文本切分成模型能理解的最小块。

对于英文,一个 Token 大约是 4 个字符(约 0.75 个单词);对于中文,一个汉字通常占 1–2 个 Token。

英文示例:
"Hello, world!" → ["Hello", ",", " world", "!"] → 4 tokens

中文示例:
"你好,世界!" → ["你好", ",", "世界", "!"] → 4–8 tokens(取决于 Tokenizer)

代码示例:
"function hello()" → ["function", " hello", "()"] → 3 tokens

📌 实践小练习

用 OpenAI 的 tiktoken 或在线工具 platform.openai.com/tokenizer 把你的系统提示词粘贴进去,看看它占多少 Token。实际结果通常会比直觉更高。

10.1.2 一次 API 调用的 Token 组成

每次调用 LLM(Large Language Model,大语言模型)时,Token 分为两部分:

flowchart LR
    A["输入 Token\n(Input / Prompt Tokens)"] --> B["LLM"]
    B --> C["输出 Token\n(Output / Completion Tokens)"]

关键点:输入 Token 和输出 Token 的单价不同。很多模型的输出 Token 通常比输入 Token 更贵,但具体倍率随厂商、模型和计费周期变化。

下表只是写作时查询到的示例价格,用于帮助你理解数量级,不应作为长期准确报价。真正上线前,请以厂商官网的实时价格为准。

模型/系列(示例) 输入价格(/1M tokens,示例) 输出价格(/1M tokens,示例) 上下文窗口(示例)
GPT-4o $2.50 $10.00 128K
GPT-4o-mini $0.15 $0.60 128K
Claude Sonnet 系列 约 $3.00 约 $15.00 约 200K
Claude Haiku 系列 约 $1.00 约 $5.00 约 200K
DeepSeek Chat / V 系列 价格变动较快,通常显著低于强模型 价格变动较快 上下文窗口随版本变化

⚠️ 价格随时变动

以上价格和窗口都仅供参考,各厂商经常调价、改模型名、改上下文长度。重要的不是记住具体数字,而是理解三个结构性规律:输入和输出分开计费;输出通常更贵;小模型和强模型的价差可能非常大。

10.1.3 上下文窗口:你的硬性约束

上下文窗口是模型单次调用能处理的最大 Token 数(输入 + 输出)。这是一个硬性约束:

加起来可能就 25000–28000 tokens,再留给模型输出的空间,实际可用不到 128K 的 1/4。


10.2 用第九章 Multi-Agent 图算一笔真实账

来算一笔账。我们用第九章的需求分析图处理这样一条需求:“我们需要一个移动端扫码签到功能,支持蓝牙围栏校验位置、批量导出签到记录为 Excel,要考虑跨境数据合规。”

步骤 调用者 输入 Token(估算) 输出 Token(估算) 累计输入
1 Triage/Classifier ~1200 ~80 1200
2 Extract Agent ~2000 ~400 3200
3 Clarify Agent ~2500 ~200 5700
4 Supervisor(选择专家) ~1500 ~100 7200
5 Functional Expert (ReAct × 2) ~3000 ~600 10200
6 Security Expert (ReAct × 3) ~4500 ~800 14700
7 Aggregator ~1800 ~200 16500
8 Risk Agent ~2200 ~500 18700
9 Summary/Critic-Refine × 2 ~5000 ~1200 23700

合计:输入 ~23,700 tokens + 输出 ~4,080 tokens

按上表示例中的 GPT-4o 价格估算:

输入成本: 23,700 / 1,000,000 × $2.50 = $0.059
输出成本: 4,080 / 1,000,000 × $10.00 = $0.041
单次请求总计: $0.100(约 ¥0.73)

单次看起来不高,但放到日常调用量里,差距会很明显:

日均 200 次需求分析:
  全部用 GPT-4o:     约 $20/天 → 约 $600/月
  全部用 GPT-4o-mini: 约 $1.2/天 → 约 $36/月
  混合模型(本章策略): 约 $3/天 → 约 $90/月

⚠️ Multi-Agent 为什么贵:上下文被反复传输

LLM API 是无状态的:每次调用都必须把“当前上下文”完整提交一次。Multi-Agent 让一次需求分析跨多个节点,于是同一份内容会在不同节点之间被反复传输。具体有 4 类重复:

所以更准确的一句话是:Multi-Agent 系统贵,不只是因为 Agent 数量多,而是因为无状态 LLM 调用让上下文、工具定义和中间推理结果在多个 Agent 之间被反复传输。后面 10.4–10.7 的省钱策略——精简提示词、裁剪历史、摘要压缩、Prompt Caching、按需注入工具——本质都在做同一件事:让同一份内容不要反复重发。

10.2.1 成本估算工具

为了让上面的“估算”变成可运行的数字,我们在项目中实现一个 Token 估算器。完整 Prompt 放在 10.2.2,下面先看参考实现:

// services/chat/src/llm/cost/token-estimator.ts
const PRICING: Record<string, { input: number; output: number; cachedInput?: number }> = {
  'gpt-4o': { input: 2.50, output: 10.00, cachedInput: 1.25 },
  'gpt-4o-mini': { input: 0.15, output: 0.60, cachedInput: 0.075 },
  'claude-sonnet': { input: 3.00, output: 15.00, cachedInput: 0.30 },
  'claude-haiku': { input: 0.80, output: 4.00, cachedInput: 0.08 },
  'deepseek-chat': { input: 0.27, output: 1.10 },
};

export function estimateTextTokens(text: string): number {
  if (!text) return 0;
  let tokens = 0;
  for (const char of text) {
    if (/[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]/.test(char)) {
      tokens += 1; // 中文字符约 1 token
    } else {
      tokens += 0.25; // 英文/数字每 4 字符约 1 token
    }
  }
  return Math.ceil(tokens);
}

export function getModelPricing(modelName: string) {
  return PRICING[modelName] || PRICING['gpt-4o-mini'];
}

export function estimateGraphNodeCost(input: {
  nodeName: string;
  modelName: string;
  systemPrompt: string;
  toolSchemas?: string;
  messages?: string;
  outputText: string;
}): { inputTokens: number; outputTokens: number; estimatedCostUsd: number } {
  const inputText = [input.systemPrompt, input.toolSchemas || '', input.messages || ''].join('\n');
  const inputTokens = estimateTextTokens(inputText);
  const outputTokens = estimateTextTokens(input.outputText);
  const pricing = getModelPricing(input.modelName);

  const estimatedCostUsd =
    (inputTokens / 1_000_000) * pricing.input +
    (outputTokens / 1_000_000) * pricing.output;

  return { inputTokens, outputTokens, estimatedCostUsd };
}

这个估算器并不追求精确,它的作用是在设计阶段快速回答“这条链路大概要花多少钱”。真正的精确数据,要靠 10.8 节的 withTokenUsage 从 provider(模型服务商)返回的 usage 元数据中读取。

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.2.1"

image.png

10.2.2 🤖 用 AI 生成本节代码


10.3 上下文为什么会膨胀

generated-image-1778414790085.png

在单 Agent 场景中,上下文通常由 system prompt(系统提示词)和 messages(消息历史)组成,增长相对线性。但在 Multi-Agent 图中,上下文膨胀是多维的

flowchart TD
    subgraph "每个节点的输入 Token 组成"
        SP["System Prompt\n~800–2000 tokens"] --> NODE["Agent 节点"]
        TOOLS["Tool Schemas\n~500–1500 tokens"] --> NODE
        STATE["State 字段\n(clarified、analysisResult…)\n累积增长"] --> NODE
        MSGS["messages(append reducer)\n每轮追加,从不删除"] --> NODE
    end

📖 本节涉及的几个工程术语

膨胀的五个驱动力

  1. State 字段累积clarifiedanalysisResultriskResult 等字段在 Annotated State 中被下游节点读取——这些都是输入 Token
  2. messages 使用 append reducer:LangGraph 的 MessagesAnnotation 默认追加消息,不删除。每个专家的对话历史都在 messages 中积累
  3. Tool schemas 随每次调用发送:Functional Expert 带 3 个工具,Security Expert 带 2 个工具——工具的 description + schema 每次调用都完整发送
  4. 每个 Expert 拿到完整的已澄清需求clarified 字段在所有专家节点中被完整传入
  5. Critic-Refine 循环倍增成本:Summary → Critic → 修改 → 再 Critic,每次循环都重新发送前面的全部上下文
flowchart LR
    A["Triage\n~1.2K"] --> B["Extract\n~2K"]
    B --> C["Clarify\n~2.5K"]
    C --> D["Supervisor\n~1.5K"]
    D --> E["Functional\n~3K"]
    D --> F["Security\n~4.5K"]
    E --> G["Aggregator\n~1.8K"]
    F --> G
    G --> H["Risk\n~2.2K"]
    H --> I["Summary+Critic\n~5K"]

    style A fill:#90EE90
    style B fill:#90EE90
    style C fill:#FFEB3B
    style D fill:#FFEB3B
    style E fill:#FF9800
    style F fill:#FF5722
    style G fill:#FFEB3B
    style H fill:#FF9800
    style I fill:#FF5722

颜色越深,输入 Token 越多。Security Expert 和 Summary/Critic 是最贵的两个节点——前者因为工具调用循环多(ReAct × 3),后者因为要读取所有前序分析结果。

📌 关键洞察

Multi-Agent 系统的 Token 成本不是各节点之和,而是会随着上下文累积不断放大。每个后续节点都要重读前序节点的输出,这就是为什么 10.2 中 9 个步骤的总输入 Token 不是简单累加,而是呈加速增长。

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.3"

image 1.png


10.4 策略一:Prompt 与工具定义瘦身

提示词是每次调用都会发送的固定成本。优化它是最容易落地、也最容易长期受益的手段之一。

10.4.1 精简系统提示词

以需求分析流水线中的 Supervisor 提示词为例:

// ❌ 冗余的提示词(约 180 tokens,按中文约 1 token/字粗估)
const verbosePrompt = `
你是一个非常专业的需求分析调度员,你的职责是根据用户提交的需求,
仔细分析需求的内容和特征,然后决定应该由哪些专家来进行评审。
你需要始终保持专业、严谨的态度。当你看到需求时,你应该仔细判断
这个需求涉及哪些领域(功能、性能、安全、合规),然后选择相应的
专家。如果你不确定是否需要某个专家,宁可多选也不要漏选。
你的选择应该有充分的理由,并且要考虑到需求的复杂性和风险性。
`;

// ✅ 精简后的提示词(约 140–160 tokens,按相同口径粗估)——来自 experts.ts 的实际实现
const concisePrompt = `你是需求分析调度员。根据已澄清的需求,判断本次需要哪些专家评审。

可选专家:
- functional:功能需求分析(任何需求都至少需要)
- performance:性能分析(涉及批量操作、大文件、实时性、高并发时选择)
- security:安全分析(涉及登录、权限、数据访问、文件上传时选择)
- compliance:合规分析(涉及跨境、个人信息、行业监管、金融/医疗时选择)`;

按中文约 1 token/字的粗略口径估算,冗余版约 180 tokens,精简版约 140–160 tokens,节省大约 10%–25%。真正明显的收益通常来自两类情况:一是把上千 token 的长角色说明压缩成结构化规则;二是在 Multi-Agent 场景下,多个子 Agent、多轮 ReAct 和 Critic-Refine 循环反复发送同一类 system prompt,单次几十 token 的节省会被调用次数放大。

10.4.2 提示词优化清单

10.4.3 工具定义优化

工具的 descriptionschema 也是输入 Token 的一部分。每次调用都会发送。以 expert-tools.ts 中的工具为例:

// ❌ 冗余的工具定义
{
  name: 'check_security_policy',
  description: '这个工具用于检查需求是否符合组织内部的安全策略和安全基线要求,它会返回安全策略命中情况和相应的合规要求,你应该在分析安全相关的需求时优先调用这个工具。',
  schema: z.object({
    requirementText: z.string().describe('用户提交的完整需求描述文本,需要包含功能描述和约束条件'),
  }),
}

// ✅ 精简后
{
  name: 'check_security_policy',
  description: '检查需求的安全策略命中情况',
  schema: z.object({
    requirementText: z.string().describe('需求描述'),
  }),
}

看起来很小的优化,但每次工具调用都省 50–100 tokens。Security Expert 一次 ReAct 循环调用 3 次工具,就是 150–300 tokens;整个图跑下来,工具定义的 Token 累计可观。


10.5 策略二:消息裁剪与摘要压缩

generated-image-1778414793500.png

这一类策略通常优先级最高。messages 往往是输入 Token 的最大来源之一,而 LangGraph 的 MessagesAnnotation 使用 append reducer——只增不减。

10.5.1 滑动窗口裁剪

最简单的方法是只保留最近的 N 条消息。关键难点是 tool_calls 与 ToolMessage 必须成对保留:如果截断了带 tool_calls 的 AIMessage,却保留了对应的 ToolMessage,provider 可能会报错,或者模型会基于不完整工具上下文继续生成。完整 Prompt 放在 10.5.4,下面先看参考实现:

📖 消息类型说明

// services/chat/src/llm/context/message-trimmer.ts
import { BaseMessage, SystemMessage } from '@langchain/core/messages';

export function trimMessagesForContext(
  messages: BaseMessage[],
  options: { maxMessages?: number; preserveSystemMessages?: boolean } = {},
): BaseMessage[] {
  const { maxMessages = 20, preserveSystemMessages = true } = options;

  const systemMsgs = preserveSystemMessages
    ? messages.filter((m) => m instanceof SystemMessage)
    : [];
  const nonSystemMsgs = messages.filter((m) => !(m instanceof SystemMessage));

  const trimmed = nonSystemMsgs.slice(-maxMessages);
  const cleaned = removeOrphanToolMessages(trimmed);
  return [...systemMsgs, ...cleaned];
}

/**
 * 按 tool_call_id 精确配对,避免 OpenAI/Anthropic 因 tool_calls 不完整报错。
 * 策略是"全有或全无":
 *   - AIMessage(tool_calls) 必须每一个 tool_call.id 都能在窗口内找到对应的
 *     ToolMessage(tool_call_id),否则整条 AIMessage 移除。
 *   - ToolMessage 仅当 tool_call_id 出现在某条幸存的 AIMessage 的 tool_calls 中
 *     时才保留。
 */
function removeOrphanToolMessages(messages: BaseMessage[]): BaseMessage[] {
  const respondedToolCallIds = new Set<string>();
  for (const msg of messages) {
    if (msg._getType() === 'tool') {
      const tcId = (msg as any).tool_call_id as string | undefined;
      if (tcId) respondedToolCallIds.add(tcId);
    }
  }

  const survivingAiIndices = new Set<number>();
  const survivingToolCallIds = new Set<string>();
  for (let i = 0; i < messages.length; i++) {
    const msg = messages[i];
    if (msg._getType() !== 'ai') continue;
    const toolCalls = (msg as any).tool_calls as Array<{ id?: string }> | undefined;
    if (!toolCalls || toolCalls.length === 0) continue;

    const allResponded = toolCalls.every(
      (tc) => tc.id && respondedToolCallIds.has(tc.id),
    );
    if (allResponded) {
      survivingAiIndices.add(i);
      for (const tc of toolCalls) if (tc.id) survivingToolCallIds.add(tc.id);
    }
  }

  const result: BaseMessage[] = [];
  for (let i = 0; i < messages.length; i++) {
    const msg = messages[i];
    const msgType = msg._getType();

    if (msgType === 'ai') {
      const toolCalls = (msg as any).tool_calls as Array<{ id?: string }> | undefined;
      if (toolCalls && toolCalls.length > 0) {
        if (survivingAiIndices.has(i)) result.push(msg);
        continue;
      }
    }

    if (msgType === 'tool') {
      const tcId = (msg as any).tool_call_id as string | undefined;
      if (tcId && survivingToolCallIds.has(tcId)) result.push(msg);
      continue;
    }

    result.push(msg);
  }
  return result;
}

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.5.1"

image 2.png

10.5.2 摘要压缩

滑动窗口会丢失早期信息。更智能的方法是:把早期对话压缩成摘要,保留关键信息。

flowchart LR
    A["完整对话\n(30 条消息)"] --> B["前 20 条\n→ 压缩为摘要"]
    B --> C["摘要 + 最近 10 条\n→ 发给模型"]

参考实现:

// services/chat/src/llm/context/conversation-compressor.ts
import { BaseMessage, SystemMessage } from '@langchain/core/messages';

export interface SummaryModel {
  invoke(messages: { role: string; content: string }[]): Promise<{ content: string }>;
}

export async function compressConversation(
  messages: BaseMessage[],
  summaryModel: SummaryModel,
  options: { keepRecent?: number; summaryMaxTokens?: number } = {},
): Promise<BaseMessage[]> {
  const { keepRecent = 10, summaryMaxTokens = 500 } = options;

  const systemMsgs = messages.filter((m) => m instanceof SystemMessage);
  const nonSystemMsgs = messages.filter((m) => !(m instanceof SystemMessage));

  if (nonSystemMsgs.length <= keepRecent) return messages;

  const earlyMsgs = nonSystemMsgs.slice(0, -keepRecent);
  const recentMsgs = nonSystemMsgs.slice(-keepRecent);

  const conversationText = earlyMsgs.map((m) => `${m._getType()}: ${m.content}`).join('\n');
  const summaryResponse = await summaryModel.invoke([
    {
      role: 'system',
      content: `把以下对话压缩为摘要,保留关键信息(需求编号、功能描述、用户意图、已完成的操作)。最多 ${summaryMaxTokens} 个 token。`,
    },
    { role: 'user', content: conversationText },
  ]);

  const summaryMsg = new SystemMessage(`[对话摘要] ${summaryResponse.content}`);
  return [...systemMsgs, summaryMsg, ...recentMsgs];
}

在图的 Agent 节点中使用:

async function expertAgentNode(state: typeof RequirementAnalysisState.State) {
  // 先裁剪,再压缩——两种策略组合使用
  const trimmed = trimMessagesForContext(state.messages, { maxMessages: 15 });
  const compressed = await compressConversation(trimmed, summaryModel, { keepRecent: 10 });

  const model = createChatModel({
    modelConfigId: 'demo-gpt-4o-mini',
    modelName: 'gpt-4o-mini',
  });
  const modelWithTools = model.bindTools(tools);
  const response = await modelWithTools.invoke([
    { role: 'system', content: expertSystemPrompt },
    ...compressed,
  ]);
  return { messages: [response] };
}

⚠️ 压缩本身也要花 Token

摘要压缩本身需要一次模型调用,所以它只在对话很长时才划算。一般规则是:当被压缩的消息超过 2000 tokens 时,压缩才更容易省钱。用小模型(如 deepseek-chat)做摘要,成本通常可以控制在很低水平。

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.5.2"

image 3.png

10.5.3 两种策略的对比

维度 滑动窗口 摘要压缩
实现复杂度 简单(纯数组操作) 中等(需要额外模型调用)
信息保留 会丢失早期信息 保留关键信息的摘要
额外成本 每次压缩一次小模型调用
适用场景 对话轮数多但早期信息不重要 早期信息包含关键上下文(如需求编号、业务约束)
推荐组合 先用摘要压缩早期对话,然后对压缩后的内容再用滑动窗口做硬截断

10.5.4 🤖 用 AI 生成本节代码


10.6 策略三:Prompt Caching 与稳定前缀

generated-image-1778414851016.png

在 system prompt、工具定义较长,并且多次调用共享稳定前缀的场景里,Prompt Caching 很值得优先考虑。

10.6.1 原理

在传统模式中,每次 API 调用都会重新处理全部输入 Token——即使 90% 的内容(系统提示词、工具定义)跟上一次完全一样。

Prompt Caching 的核心思想:如果输入的前缀部分没变,就复用上次的计算结果,不重新计算。

flowchart TD
    subgraph "传统模式"
        A1["调用 1\nSystem + Tools + History + Input"] --> B1["完整计算\n→ 全价"]
        A2["调用 2\nSystem + Tools + History' + Input'"] --> B2["完整计算\n→ 全价"]
    end
    subgraph "Prompt Caching"
        C1["调用 1\nSystem + Tools + History + Input"] --> D1["完整计算\n→ 全价\n→ 缓存前缀"]
        C2["调用 2\n[cached] System + Tools + History' + Input'"] --> D2["复用前缀计算\n→ 折扣价"]
    end

10.6.2 各厂商的 Prompt Caching 实现

厂商 机制 缓存命中折扣(示例) 最小缓存前缀(示例) 缓存过期时间(示例)
OpenAI 自动(无需配置) 输入价 50% off 1024 tokens 5–10 分钟
Anthropic (Claude) 显式标记 cache_control cache read 约为普通输入价的 10%;首次 cache write 通常比普通输入更贵 Haiku 通常约 1024 tokens;Sonnet/Opus 通常约 2048 tokens 常见 5 分钟
DeepSeek 自动识别相同 prefix cache hit 价格通常显著低于 cache miss 随版本变化,实践中通常需要较长稳定前缀 短时间内

这里同样不要把表格当成长期准确资料。Prompt Caching 的支持模型、最低 Token 数、过期时间和折扣都会变化,发布前应重新核对对应厂商文档。

📖 Prompt Caching 相关术语

10.6.3 Prompt Caching 的工作流程

以 OpenAI 为例,缓存是自动的:

第 1 次调用(Functional Expert):
  输入: [System Prompt (800 tokens)] + [Tools (1200 tokens)] + [User: "分析此需求"]
  全部计算 → 缓存 System + Tools 前缀
  成本: 2100 tokens × $2.50/M = $0.00525

第 2 次调用(同一专家 ReAct 循环第 2 轮,5 分钟内):
  输入: [System Prompt (800)] + [Tools (1200)] + [History + ToolMessage + User]
  前 2000 tokens 命中缓存 → 半价
  成本: 2000 × $1.25/M + 800 × $2.50/M = $0.0045

  节省了 ~14%

10.6.4 如何最大化 Prompt Caching 效果

核心原则:把不变的内容放在前面,变化的内容放在后面。

// ✅ 好的顺序(缓存命中率高)
const messages = [
  // 1. System Prompt(不变) → 被缓存
  { role: 'system', content: EXPERT_SYSTEM_PROMPT },
  // 2. 工具定义(不变) → 被缓存
  // (bindTools 会自动放在 system 之后)
  // 3. 对话历史(变化) → 前缀部分可能被缓存
  ...conversationHistory,
  // 4. 用户新输入(变化) → 不会被缓存
  { role: 'user', content: clarifiedRequirement },
];

// ❌ 差的顺序(缓存命中率低)
const messages = [
  { role: 'user', content: clarifiedRequirement },    // 变化的内容放前面
  { role: 'system', content: EXPERT_SYSTEM_PROMPT },   // 不变的内容放后面
  // → 缓存无法命中,因为前缀每次都不同
];

对于 Anthropic 的 Claude,需要显式标记缓存点。Anthropic API 的 system 字段支持两种写法:

要做 Prompt Caching,必须用第二种写法:

const response = await anthropic.messages.create({
  model: 'claude-sonnet-4-20250514',
  max_tokens: 1024,
  // 注意:必须用 block array 形式,cache_control 才能挂在 block 上;
  // 不能写成 system: 'You are ...' 这种字符串形式。
  system: [
    {
      type: 'text',
      text: EXPERT_SYSTEM_PROMPT,
      cache_control: { type: 'ephemeral' },
    },
  ],
  messages: [
    ...conversationHistory,
    { role: 'user', content: clarifiedRequirement },
  ],
});

// 响应中可以看到缓存统计
console.log(response.usage);
// {
//   input_tokens: 3500,
//   cache_creation_input_tokens: 2000,  // 首次创建缓存(按更高单价计费)
//   cache_read_input_tokens: 1500,      // 后续读取缓存(按 0.1x 单价计费)
//   output_tokens: 200,
// }

注:Anthropic 的最小可缓存前缀有长度门槛(具体数值以官方文档为准),太短的 system prompt 即使挂了 cache_control 也不会真的进缓存——这是踩坑点。

📌 Prompt Caching 的核心理解

可以把 Prompt Caching 近似理解为:厂商复用了稳定前缀的中间计算结果(常被解释为 KV Cache 一类的机制),而不是缓存模型最终输出。所以它通常不改变回复内容,只是降低重复前缀的输入处理成本。不同厂商的底层实现不一定完全相同,文档中只需要抓住“稳定前缀可复用”这个工程原则。

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.6.4"

image 4.png

10.6.5 缓存命中率监控(可选)

cache-monitor.ts 不是当前项目已经存在的必需文件,它只是一个可选观测示例,用来解释“如何把 provider 返回的 cache usage 变成命中率”。如果你已经在 10.8 的 token_usages.cachedInputTokens 里记录了缓存命中 token,就不一定需要单独创建这个文件。

// 可选示例:services/chat/src/llm/cost/cache-monitor.ts

interface CacheStats {
  totalCalls: number;
  cacheHits: number;
  cacheMisses: number;
  tokensSaved: number;
  moneySaved: number;
}

const cacheStats: CacheStats = {
  totalCalls: 0,
  cacheHits: 0,
  cacheMisses: 0,
  tokensSaved: 0,
  moneySaved: 0,
};

export function recordCacheResult(usage: {
  input_tokens: number;
  cache_read_input_tokens?: number;
  cache_creation_input_tokens?: number;
}) {
  cacheStats.totalCalls++;

  if (usage.cache_read_input_tokens && usage.cache_read_input_tokens > 0) {
    cacheStats.cacheHits++;
    cacheStats.tokensSaved += usage.cache_read_input_tokens;
    cacheStats.moneySaved += (usage.cache_read_input_tokens / 1_000_000) * 3.00 * 0.9;
  } else {
    cacheStats.cacheMisses++;
  }
}

export function getCacheStats() {
  return {
    ...cacheStats,
    hitRate: cacheStats.totalCalls > 0
      ? (cacheStats.cacheHits / cacheStats.totalCalls * 100).toFixed(1) + '%'
      : '0%',
  };
}

10.7 策略四:模型分级与节点级模型选择

generated-image-1778414839172.png

不是所有 Agent 节点都需要最强的模型。更稳妥的做法是按角色分配模型:高风险节点保留强模型,低复杂度节点使用更便宜的模型。

10.7.1 模型分级原则

flowchart TD
    A["Agent 节点"] --> B{"角色\n复杂度"}
    B -->|"高风险/复杂推理\n(Supervisor、Critic、Security)"| C["强模型\ngpt-4o / claude-sonnet\n示例:数美元/M input"]
    B -->|"中等\n(Functional、Risk)"| D["中模型\ngpt-4o-mini\n示例:低于强模型一个数量级"]
    B -->|"低复杂度\n(Compressor、简单分类)"| E["轻量模型\ndeepseek-chat\n示例:低成本模型"]

10.7.2 AgentModelSet:按角色配置默认模型

我们用一个 AgentModelSet 接口来声明每种角色对应哪个 modelConfigId。这里的 modelConfigId 指数据库或配置文件中的模型配置 ID,而不是模型名称本身。完整 Prompt 放在 10.7.6,先看参考实现:

// services/chat/src/llm/cost/agent-model-set.ts

export interface AgentModelSet {
  supervisorModelConfigId: string;
  functionalModelConfigId: string;
  performanceModelConfigId: string;
  securityModelConfigId: string;
  complianceModelConfigId: string;
  riskModelConfigId: string;
  summaryModelConfigId: string;
  criticModelConfigId: string;
  compressorModelConfigId: string;
}

export const DEFAULT_AGENT_MODEL_SET: AgentModelSet = {
  supervisorModelConfigId: 'demo-gpt-4o',       // 调度决策需要强推理
  functionalModelConfigId: 'demo-gpt-4o-mini',   // 功能分析中等复杂度
  performanceModelConfigId: 'demo-gpt-4o-mini',  // 性能分析有工具辅助
  securityModelConfigId: 'demo-gpt-4o',          // 安全分析必须严谨
  complianceModelConfigId: 'demo-gpt-4o',        // 合规分析法律敏感
  riskModelConfigId: 'demo-gpt-4o-mini',         // 风险汇总中等复杂度
  summaryModelConfigId: 'demo-gpt-4o',           // 最终报告质量要求高
  criticModelConfigId: 'demo-gpt-4o',            // 质量审查需要强推理
  compressorModelConfigId: 'demo-deepseek-chat',  // 摘要压缩用最便宜的
};

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.7.2"

image 5.png

10.7.3 两层模型选择:默认查表 + 运行时覆盖

resolveModelForAgent 实现两层逻辑:

// services/chat/src/llm/cost/agent-model-set.ts(续)

const HIGH_RISK_AGENTS: AgentName[] = [
  'supervisor', 'security_expert', 'compliance_expert', 'critic', 'summary_agent',
];

export function resolveModelForAgent(input: {
  agentName: AgentName;
  defaultModelSet?: AgentModelSet;
  requirementComplexity?: 'low' | 'medium' | 'high';
  budgetStatus?: { usedPercent: number };
}): { selectedModelConfigId: string; overrideReason: string | null } {
  const modelSet = input.defaultModelSet || DEFAULT_AGENT_MODEL_SET;
  const configKey = AGENT_TO_CONFIG_KEY[input.agentName];
  const defaultId = modelSet[configKey];
  const isHighRisk = HIGH_RISK_AGENTS.includes(input.agentName);
  const budgetPercent = input.budgetStatus?.usedPercent ?? 0;

  // 层 1:预算超限
  if (budgetPercent >= 100) {
    if (input.agentName === 'compressor') {
      return { selectedModelConfigId: defaultId, overrideReason: null }; // compressor 豁免
    }
    return { selectedModelConfigId: defaultId, overrideReason: 'budget_exceeded_reject' };
  }

  // 层 2:预算紧张(80–100%),非高风险 Agent 降级
  if (budgetPercent >= 80 && !isHighRisk) {
    return {
      selectedModelConfigId: modelSet.compressorModelConfigId,
      overrideReason: `budget_tight_downgrade (${budgetPercent}%)`,
    };
  }

  // 层 3:低复杂度需求,非高风险 Agent 降级
  if (input.requirementComplexity === 'low' && !isHighRisk) {
    return {
      selectedModelConfigId: modelSet.compressorModelConfigId,
      overrideReason: 'low_complexity_downgrade',
    };
  }

  // 默认:按角色查表
  return { selectedModelConfigId: defaultId, overrideReason: null };
}

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.7.3"

image 6.png

10.7.4 可以这样升级 Supervisor 子图:从单模型到 ModelSet

第九章的 createAnalysisSupervisorSubGraph 接受一个 model 参数——所有专家共用同一个模型。当前仓库新增了 AgentModelSetresolveModelForAgent,但还没有把它们接入主图。后续如果要继续工程化,可以按下面的方式升级,让每个专家使用不同模型:

// 第九章:所有专家用同一个 model
export function createAnalysisSupervisorSubGraph(model: BaseChatModel) {
  const functionalExpert = createFunctionalExpert(model);  // 都用同一个
  const securityExpert = createSecurityExpert(model);       // 都用同一个
  // ...
}

// 可选升级:每个专家按角色用不同 model
export function createAnalysisSupervisorSubGraph(modelSet: AgentModelSet) {
  const functionalModel = createChatModel({
    modelConfigId: modelSet.functionalModelConfigId,
    modelName: resolveModelName(modelSet.functionalModelConfigId),
  });
  const securityModel = createChatModel({
    modelConfigId: modelSet.securityModelConfigId,
    modelName: resolveModelName(modelSet.securityModelConfigId),
  });

  const functionalExpert = createFunctionalExpert(functionalModel);
  const securityExpert = createSecurityExpert(securityModel);
  // ...
}

10.7.5 模型分级后的成本对比

方案 单次请求成本(示例估算) 月度成本(200 次/天,示例估算)
全部用 GPT-4o ~$0.100 ~$600
Supervisor/Critic 用 4o + Expert 用 4o-mini ~$0.025 ~$150
上面 + Prompt Caching ~$0.018 ~$108
上面 + 消息裁剪/压缩 ~$0.015 ~$90

在这组示例假设下,从约 $600 到约 $90,成本下降约 85%。真实比例取决于模型价格、缓存命中率、输出长度和实际调用次数。

10.7.6 🤖 用 AI 生成本节代码


10.8 策略五:节点级 Token usage 采集与数据库记录

前面的策略主要解决“如何降低成本”。但到底省了多少、哪个节点最贵、哪类 Agent 最容易超预算,这些问题都需要数据来回答。

10.8.1 三层设计

flowchart TD
    A["模型调用\n(ChatOpenAI.invoke)"] --> B["withTokenUsage 包装器\n读取 response_metadata.usage\n或回退估算"]
    B --> C["TokenUsageService\n写入 token_usages 表"]
    C --> D["按 graph/node/agent 维度查询"]

📖 本节涉及的几个工程术语

10.8.2 Prisma 数据模型

// services/chat/prisma/schema.prisma

model token_usages {
  id                String   @id @default(cuid())
  conversationId    String?  @db.VarChar(255)
  messageId         String?  @db.VarChar(255)
  threadId          String?  @db.VarChar(255)
  graphName         String   @db.VarChar(100)
  nodeName          String   @db.VarChar(100)
  agentName         String   @db.VarChar(100)
  modelConfigId     String?  @db.VarChar(255)
  modelName         String   @db.VarChar(100)
  provider          String   @default("openai") @db.VarChar(50)
  inputTokens       Int      @default(0)
  outputTokens      Int      @default(0)
  totalTokens       Int      @default(0)
  cachedInputTokens Int      @default(0)
  estimatedCostUsd  Float    @default(0)
  isEstimated       Boolean  @default(false)
  latencyMs         Int      @default(0)
  overrideReason    String?
  createdAt         DateTime @default(now())

  @@index([conversationId])
  @@index([graphName, nodeName])
  @@index([agentName])
  @@index([modelConfigId])
  @@index([createdAt])
}

关键字段说明:

10.8.3 TokenUsageService

完整 prompt 在 10.8.5,先看参考实现:

// services/chat/src/llm/cost/token-usage.service.ts
import { PrismaClient } from '@prisma/client';

export class TokenUsageService {
  constructor(private prisma: PrismaClient) {}

  async recordUsage(record: TokenUsageRecord): Promise<void> {
    try {
      await this.prisma.token_usages.create({
        data: {
          conversationId: record.conversationId,
          messageId: record.messageId,
          threadId: record.threadId,
          graphName: record.graphName,
          nodeName: record.nodeName,
          agentName: record.agentName,
          modelConfigId: record.modelConfigId,
          modelName: record.modelName,
          provider: record.provider || 'openai',
          inputTokens: record.inputTokens,
          outputTokens: record.outputTokens,
          totalTokens: record.totalTokens ?? (record.inputTokens + record.outputTokens),
          cachedInputTokens: record.cachedInputTokens || 0,
          estimatedCostUsd: record.estimatedCostUsd,
          isEstimated: record.isEstimated || false,
          latencyMs: record.latencyMs || 0,
          overrideReason: record.overrideReason,
        },
      });
    } catch (err) {
      console.warn('[TokenUsageService] recordUsage failed, skipping:', err);
    }
  }

  async getMonthlyStats(): Promise<MonthlyStats> {
    const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
    const records = await this.prisma.token_usages.findMany({
      where: { createdAt: { gte: monthStart } },
    });
    return {
      totalCost: records.reduce((s, r) => s + r.estimatedCostUsd, 0),
      totalInputTokens: records.reduce((s, r) => s + r.inputTokens, 0),
      totalOutputTokens: records.reduce((s, r) => s + r.outputTokens, 0),
      totalCachedTokens: records.reduce((s, r) => s + r.cachedInputTokens, 0),
      calls: records.length,
    };
  }
}

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.8.3"

image 7.png

10.8.4 withTokenUsage 包装器

这一层负责把“模型调用”与“DB 记录”连接起来。当前项目已经提供了独立的 withTokenUsage 工具,但尚未把它接入 experts.tsOrchestratorService 的主流程。它会包装一个 async 函数,优先从 LangChain 的 response_metadata 中读取 usage;如果 provider 没有返回 usage,再回退到估算逻辑。参考实现:

// services/chat/src/llm/cost/with-token-usage.ts
import { TokenUsageService, TokenUsageRecord } from './token-usage.service';
import { estimateTextTokens, getModelPricing } from './token-estimator';

export async function withTokenUsage<T>(
  options: WithTokenUsageOptions,
  usageService: TokenUsageService | null,
  fn: () => Promise<T>,
): Promise<T> {
  const start = Date.now();
  const result = await fn();
  const latencyMs = Date.now() - start;

  if (!usageService) return result;

  try {
    const usage = extractUsageFromResponse(result);
    const pricing = getModelPricing(options.modelName);

    let inputTokens: number, outputTokens: number, cachedInputTokens: number, isEstimated: boolean;

    if (usage) {
      inputTokens = usage.inputTokens;
      outputTokens = usage.outputTokens;
      cachedInputTokens = usage.cachedInputTokens || 0;
      isEstimated = false;
    } else {
      // provider 没回 usage 时的兜底估算。
      // 倍率 5 来自 10.2 的真实样本(输入 ≈ 5.8x 输出)取保守圆整,
      // 不同场景在 3-7 之间波动;估算只是兜底,应优先依赖 provider 真实 usage。
      const content = extractContentFromResponse(result);
      outputTokens = estimateTextTokens(content);
      inputTokens = outputTokens * 5;
      cachedInputTokens = 0;
      isEstimated = true;
    }

    const normalInputTokens = inputTokens - cachedInputTokens;
    const estimatedCostUsd =
      (normalInputTokens / 1_000_000) * pricing.input +
      (cachedInputTokens / 1_000_000) * (pricing.cachedInput || pricing.input) +
      (outputTokens / 1_000_000) * pricing.output;

    await usageService.recordUsage({
      ...options,
      inputTokens, outputTokens,
      totalTokens: inputTokens + outputTokens,
      cachedInputTokens, estimatedCostUsd, isEstimated, latencyMs,
    });
  } catch (err) {
    console.warn('[withTokenUsage] Failed to record usage, skipping:', err);
  }

  return result;
}

如果后续要接入专家子图,可以这样改:

// 可选升级:experts.ts 中的 agentNode
async function agentNode(state: typeof RequirementAnalysisState.State) {
  const modelWithTools = model.bindTools?.(tools) || model;

  const response = await withTokenUsage(
    {
      graphName: 'requirement-analysis',
      nodeName: `${opts.name}_expert`,
      agentName: `${opts.name}_expert`,
      modelName: 'gpt-4o-mini',
      modelConfigId: 'demo-gpt-4o-mini',
    },
    tokenUsageService,
    () => modelWithTools.invoke([
      { role: 'system', content: systemPrompt },
      { role: 'user', content: `已澄清的需求:${JSON.stringify(state.clarified)}` },
    ]),
  );

  return { messages: [response] };
}

📌 设计原则:记录失败不影响主流程

withTokenUsagetry/catch 确保:即使数据库写入失败(网络问题、表不存在),模型调用结果也照常返回。Token 采集是“尽力而为”的辅助能力,绝不能成为业务流程的阻塞点。注意:这一节展示的是接入方式,不表示主图已经完成接入。

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.8.4"

image 8.png

10.8.5 🤖 用 AI 生成本节代码


10.9 策略六:预算阈值、模型降级与运行时覆盖

generated-image-1778414839777.png

有了 Token 数据(10.8),也有了模型分级(10.7),现在可以把它们连起来:当预算紧张时自动降级模型,预算耗尽时拒绝调用。

📖 预算策略里的三个动作

10.9.1 预算策略选择器

完整 prompt 在 10.9.4,先看参考实现:

// services/chat/src/llm/cost/budget-policy.ts

export type BudgetAction = 'allow' | 'downgrade' | 'reject';

const HIGH_RISK_AGENTS = ['supervisor', 'security_expert', 'compliance_expert', 'critic', 'summary_agent'];

export function resolveBudgetAction(input: {
  budgetUsedPercent: number;
  agentName: string;
  requirementRiskLevel?: 'low' | 'medium' | 'high';
}): { action: BudgetAction; reason: string } {
  const { budgetUsedPercent, agentName, requirementRiskLevel } = input;

  // < 80%:正常
  if (budgetUsedPercent < 80) {
    return { action: 'allow', reason: `budget OK (${budgetUsedPercent}%)` };
  }

  // 80–100%:根据 Agent 风险等级决定
  if (budgetUsedPercent < 100) {
    const isHighRisk = HIGH_RISK_AGENTS.includes(agentName);
    if (isHighRisk) {
      return { action: 'allow', reason: `high-risk agent, no downgrade (${budgetUsedPercent}%)` };
    }
    return { action: 'downgrade', reason: `budget tight, low-risk agent can downgrade (${budgetUsedPercent}%)` };
  }

  // >= 100%:compressor 豁免(它本身就是省钱的工具),其余拒绝
  if (agentName === 'compressor') {
    return { action: 'allow', reason: 'compressor allowed even over budget (cost reduction purpose)' };
  }
  return { action: 'reject', reason: `budget exceeded (${budgetUsedPercent}%)` };
}

📋 本节配套用例bun test test/chapter10-token-economics.spec.ts -t "10.9.1"

image 9.png

10.9.2 预算策略的设计决策

为什么有些 Agent 预算紧张时不降级?

Agent 预算紧张(80–100%) 预算耗尽(≥100%) 理由
supervisor ✅ 不降级 ❌ 拒绝 调度决策错误影响全局
security_expert ✅ 不降级 ❌ 拒绝 安全分析不能用弱模型
compliance_expert ✅ 不降级 ❌ 拒绝 合规分析法律敏感
critic ✅ 不降级 ❌ 拒绝 质量审查降级则形同虚设
summary_agent ✅ 不降级 ❌ 拒绝 最终报告质量要求高
functional_expert ⬇️ 降级 ❌ 拒绝 功能分析可容忍质量下降
performance_expert ⬇️ 降级 ❌ 拒绝 有工具辅助,弱模型也能做
risk_agent ⬇️ 降级 ❌ 拒绝 风险汇总中等复杂度
compressor ✅ 不降级 ✅ 豁免 它本身就是省钱用的

10.9.3 可以这样接入图节点

下面是接入思路示例,不表示当前主图已经完成这段集成。真实接入时要把 tokenUsageServiceMONTHLY_BUDGETresolveModelNameoutputField 放到当前服务已有的依赖注入和状态结构里。

async function expertNodeWithBudget(state, agentName: AgentName) {
  const monthlyStats = await tokenUsageService.getMonthlyStats();
  const budgetPercent = (monthlyStats.totalCost / MONTHLY_BUDGET) * 100;

  // 预算策略
  const budgetResult = resolveBudgetAction({
    budgetUsedPercent: budgetPercent,
    agentName,
  });
  if (budgetResult.action === 'reject') {
    return { [outputField]: `[${agentName} 因预算耗尽被跳过] ${budgetResult.reason}` };
  }

  // 模型选择
  const modelResult = resolveModelForAgent({
    agentName,
    budgetStatus: { usedPercent: budgetPercent },
  });

  const model = createChatModel({
    modelConfigId: modelResult.selectedModelConfigId,
    modelName: resolveModelName(modelResult.selectedModelConfigId),
  });

  // 执行 + 记录
  const response = await withTokenUsage(
    {
      graphName: 'requirement-analysis',
      nodeName: `${agentName}`,
      agentName,
      modelName: resolveModelName(modelResult.selectedModelConfigId),
      overrideReason: modelResult.overrideReason ?? undefined,
    },
    tokenUsageService,
    () => model.invoke([...]),
  );

  return { messages: [response] };
}

⚠️ 降级后的质量标注

当模型被降级时,overrideReason 会被写入 token_usages 表。前端可以在 UI meta 中标注“此分析使用了降级模型”,让用户知道质量可能低于正常水平。

10.9.4 🤖 用 AI 生成本节代码


10.10 工程落地清单与常见坑

前面六类策略已经分别讲过,这里不再重复每个实现细节,只给一张排查路径图:先判断成本主要花在哪里,再选择对应手段。

flowchart TD
    A["成本过高"] --> B{"最大开销\n在哪里?"}
    B -->|"系统提示词很长"| C["策略一:\n精简提示词与工具定义"]
    B -->|"对话轮数多\n消息累积"| D["策略二:\n滑动窗口 + 摘要压缩"]
    B -->|"调用次数多\n前缀重复"| E["策略三:\nPrompt Caching"]
    B -->|"全部用贵模型"| F["策略四:\n模型分级 AgentModelSet"]
    B -->|"不知道钱花在哪"| G["策略五:\n节点级 Token 采集"]
    B -->|"无法自动控制"| H["策略六:\n预算阈值 + 自动降级"]
    C --> I["还不够?"]
    D --> I
    E --> I
    F --> I
    G --> I
    H --> I
    I --> J["组合使用所有策略"]

落地建议:

  1. 先做无侵入改造:模型分级、Prompt Caching 和提示词瘦身通常不需要改动主业务流程,适合作为第一轮优化。
  2. 再处理上下文增长:如果成本主要来自多轮对话、ReAct 循环或长工具返回,再引入滑动窗口和摘要压缩。
  3. 最后补齐治理闭环:Token 采集和预算控制更偏运行时治理,适合在系统稳定后接入,用来持续发现异常成本和自动兜底。

常见问题 FAQ

Q: Token usage 记录失败怎么办?

A: withTokenUsagecatch 会将失败降级为 console.warn,不中断主流程。如果需要更强的保障,可以增加本地日志文件或异步队列作为二级备份。但不要让 Token 采集成为业务阻塞点。

Q: 模型降级后质量下降怎么办?

A: 两个手段:(1) overrideReason 写入 token_usages 表,前端在 UI meta 中标注降级原因,让用户知情;(2) HIGH_RISK_AGENTS 清单确保安全、合规等关键 Agent 永远不被降级。

Q: Prompt Caching 命中率低怎么办?

A: 检查消息顺序——System Prompt 和工具定义必须在最前面。如果使用 Anthropic Claude,确保 cache_control 标记在正确位置。另外,同一用户的连续请求间隔不要超过缓存过期时间(OpenAI 5–10 分钟,Claude 5 分钟)。

Q: 压缩摘要丢失了关键信息怎么办?

A: (1) 调大 summaryMaxTokens 允许摘要更长;(2) 只在超过 10 轮对话时才触发压缩(通过 keepRecent 控制);(3) 关键业务信息(需求编号、约束条件)应该写入 State 的独立字段(如 clarified),而不是只存在 messages 中。


10.11 本章总结

这一章的核心,是把 Token 从“看不见的消耗”变成一项可以度量、可以治理的工程资源。

和前面章节相比,本章不再只关注“Agent 能不能完成任务”,而是把注意力转向另一个生产环境里绕不开的问题:每一次模型调用背后,都有上下文长度、调用次数、模型价格和运行时策略共同决定的成本结构

可以把本章内容收束成三层:

  1. 先算清楚账:用成本估算器和节点级 usage 记录,知道一次 Multi-Agent 流水线到底在哪些节点花了钱。
  2. 再压住上下文:通过提示词瘦身、工具定义精简、消息裁剪、摘要压缩和 Prompt Caching,减少重复发送的内容。
  3. 最后建立治理机制:用 AgentModelSet 做模型分级,用 resolveBudgetAction 做预算判断,让系统在预算紧张时能降级,在预算耗尽时能拒绝高成本调用。

这样,第九章搭起来的 Multi-Agent 图就不只是“能跑”,而是开始具备生产系统需要的成本意识:知道钱花在哪里,知道哪些调用可以省,知道什么时候必须保质量,也知道什么时候应该停下来。

本章配套的 testcase 也延续这个原则:它们没有接入真实 LLM,而是采用 mock-first 的方式验证工程逻辑。这样设计有三个好处:

⚠️ 关于 Mermaid 流程图的显示

本文包含大量 Mermaid 语法的流程图和架构图。部分平台(如微信公众号、某些博客系统)可能无法直接渲染 Mermaid 图表,会显示为代码块。如果你看到的是原始代码而非图形,可以:


下一步:进入 RAG

成本可控之后,Agent 系统还需要解决另一个问题:知识从哪里来。下一章会进入 RAG(检索增强生成),让 Agent 从外部知识库中检索相关内容,而不是把所有信息都提前塞进上下文窗口。换句话说,RAG 不只是能力增强,也是一种更可控的上下文管理方式。

写在最后 🧪

这里是言萧凡的 AI 编程实验室。我会在这里持续记录和分享 AI 工具、编程实践,以及那些值得沉淀下来的高效工作方法。不只聊概念,也尽量分享能直接上手、能够复用的经验。希望这间小小的实验室,能陪你一起探索、实践和成长。2026 年,一起进步。

有兴趣的话可以添加我的微信号【Cookieboty】一起交流,不仅是编程也可以是畅谈人生。