1. 长会话 Agent 为什么越聊越“失忆”:上下文膨胀的真实代价
如果你正在跑一个多轮对话 Agent,大概率遇到过这种诡异现象:前 5 轮还答得好好的,到第 8 轮突然把用户 A 的订单信息安到了用户 B 头上,或者开始重复回答三轮前已经解决的问题。你去看日志,模型没换、提示词没改、温度也没动,唯一变化的是——上下文越来越长。
这就是 Agent 长会话里的上下文膨胀问题。它带来的不只是“答非所问”,还有两个更隐蔽的代价:一是成本失控,输入 Token 随轮次线性累积,单轮调用成本可能三天翻三倍;二是响应漂移,延迟从 200ms 涨到 2s,用户体感直接崩掉。很多人第一反应是换更大的上下文窗口,从 16K 换到 128K,但这本质上是把问题往后拖——窗口利用率超过 70% 之后,注意力机制会被大量无关片段干扰,推理准确率反而下降。
Harness Engineering 体系下的上下文清理机制,解决的正是这个问题:不是无脑删,而是在保真、成本、效率之间找平衡点。它给每个上下文片段打标签、算价值分、按当前利用率动态决策,把真正有用的信息留下,把过期、重复、无关的片段清掉。实测下来,在保证核心信息保真度 95% 以上的前提下,上下文 Token 量平均能降 70% 左右,延迟和成本同步下降。
这篇文章面向的是正在做多轮对话 Agent、工具调用 Agent 或工作流 Agent 的开发者。我会从清理机制的核心原理讲起,给出可复制的配置片段和 Python 实现,再结合 TaoToken 统一 Key 通道完成多模型调用的落地配置,最后给出上下文裁剪前后的对比验证动作。你可以直接把这套方案搬进自己的 Agent 工作流里复现。
2. TaoToken 统一 Key 通道:多模型 Agent 的前置准备
在讲清理机制之前,先把模型调用通道理顺。因为上下文清理机制本身会用到两类模型:一类是主推理模型(比如 GPT-4o、Claude 3.5),另一类是做摘要压缩和核心信息提取的轻量模型(比如 7B 级开源模型或 GPT-3.5)。如果每个模型都单独配一套 Key 和 Base URL,Agent 代码里会到处散落凭证,维护成本很高。
TaoToken 在这里的作用是提供一个统一的 Key/API 通道,让你用一套凭证调用多个模型。它的 API 地址是https://taotoken.net/api,兼容 OpenAI 的接口格式,所以现有的openaiSDK 基本不用改代码,只需要把base_url和api_key换掉即可。
2.1 为什么 Agent 场景特别需要统一通道
Agent 工作流里模型调用有几个特点:调用频次高、模型种类多、需要快速切换。比如主推理用 Claude 3.5 Sonnet,摘要压缩用 GPT-3.5-turbo,核心信息提取用本地小模型。如果每个模型都走不同的供应商,你会遇到几个麻烦:
第一,凭证管理分散。每个供应商一套 Key,轮换、限额、监控都要单独做。第二,接口格式不统一。有的用 OpenAI 格式,有的用 Anthropic 原生格式,代码里要写多套适配层。第三,成本核算困难。账单分散在多个后台,很难算清一个 Agent 任务到底花了多少钱。
统一 Key 通道把这些收敛到一处:一套 Key、一个 Base URL、一种接口格式。你可以在配置里通过model参数切换模型,代码逻辑不变。对于上下文清理机制来说,这意味着摘要压缩和核心信息提取可以复用同一套调用封装,不用为每个模型写单独的客户端。
2.2 获取 Key 与基础配置
你需要先在 TaoToken 控制台创建一个 API Key。控制台地址是https://taotoken.net/console,创建 Key 的页面在https://taotoken.net/api-keys。创建完成后,你会拿到一个以sk-开头的字符串,这就是你的统一凭证。
拿到 Key 之后,基础配置只需要两个环境变量:
export TAOTOKEN_API_KEY="sk-你的实际Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你用的是 Python 的openaiSDK(1.x 版本),初始化客户端的方式如下:
from openai import OpenAI import os client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url=os.environ["TAOTOKEN_BASE_URL"], ) # 主推理模型调用 response = client.chat.completions.create( model="claude-3-5-sonnet-20241022", messages=[{"role": "user", "content": "你好"}], temperature=0.0, ) print(response.choices[0].message.content)这里的关键点是base_url指向https://taotoken.net/api,model参数填你要用的模型 ID。切换模型时只改model字段,其他代码不动。对于上下文清理机制里的摘要压缩步骤,你只需要把model换成gpt-3.5-turbo或对应的轻量模型即可。
2.3 在 Agent 项目里组织配置
实际项目里,我建议把模型配置抽成一个独立的配置文件,而不是散落在代码各处。可以用 JSON 或 TOML 格式,下面是一个 JSON 示例,路径放在项目根目录的config/models.json:
{ "provider": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY" }, "models": { "main_reasoning": { "model_id": "claude-3-5-sonnet-20241022", "temperature": 0.0, "max_tokens": 4096 }, "summarizer": { "model_id": "gpt-3.5-turbo", "temperature": 0.0, "max_tokens": 512 }, "core_extractor": { "model_id": "gpt-3.5-turbo", "temperature": 0.0, "max_tokens": 256 } } }然后在代码里读取这个配置,初始化多个客户端实例(其实可以共用一个客户端,只是调用时传不同的model):
import json import os from openai import OpenAI with open("config/models.json", "r", encoding="utf-8") as f: cfg = json.load(f) client = OpenAI( api_key=os.environ[cfg["provider"]["api_key_env"]], base_url=cfg["provider"]["base_url"], ) def call_model(role: str, messages: list) -> str: model_cfg = cfg["models"][role] resp = client.chat.completions.create( model=model_cfg["model_id"], messages=messages, temperature=model_cfg["temperature"], max_tokens=model_cfg["max_tokens"], ) return resp.choices[0].message.content这样你的上下文清理机制里,摘要压缩调用call_model("summarizer", ...),核心信息提取调用call_model("core_extractor", ...),主推理调用call_model("main_reasoning", ...)。三件套(Base URL + Key + Model ID)都在配置里,换模型只改 JSON。
3. 可复制的上下文清理配置:标签、评分与决策片段
这一节给出可以直接复制到项目里的配置片段和核心代码。整个清理机制分四步:标签、评估、决策、验证。我们逐个落地。
3.1 上下文片段的元数据标签
每个进入上下文池的片段都要打标签。标签分四类:固有属性(类型、优先级、关联实体)、生命周期(TTL)、业务规则(比如含身份证号自动标 P0)、动态属性(与当前任务的相关性、冗余度)。
下面是一个可直接用的 Python 数据结构,保存为context_fragment.py:
from enum import Enum from datetime import datetime from typing import List, Optional class ContextType(Enum): SYSTEM_PROMPT = "system_prompt" USER_INPUT = "user_input" THINKING_CHAIN = "thinking_chain" TOOL_RESPONSE = "tool_response" INTERMEDIATE_STATE = "intermediate_state" class Priority(Enum): P0 = 1.0 # 核心信息,永久保留 P1 = 0.7 # 重要信息,尽量保留 P2 = 0.3 # 一般信息,可压缩 P3 = 0.1 # 冗余信息,可删除 class ContextFragment: def __init__( self, content: str, context_type: ContextType, priority: Priority, ttl: int = -1, related_entities: Optional[List[str]] = None, create_time: Optional[datetime] = None, ): self.id = id(self) self.content = content self.context_type = context_type self.priority = priority self.ttl = ttl self.related_entities = related_entities or [] self.create_time = create_time or datetime.now() self.value_score: float = 0.0 self.tokens: int = max(1, len(content) // 3) # 中文近似 Token 数这里tokens用len(content) // 3做近似,中文场景下 1 个汉字大约 0.3 到 0.5 个 Token,这个估算够用。如果你要精确计算,可以接入 tiktoken,但会增加依赖,Agent 场景下近似值足够。
3.2 价值评分模型
价值得分公式是四个维度的加权和:相关性、重要性、时效性、冗余度。默认权重[0.4, 0.3, 0.2, 0.1],你可以按业务调整。保存为value_scorer.py:
import numpy as np from datetime import datetime from typing import List from sentence_transformers import SentenceTransformer from context_fragment import ContextFragment model = SentenceTransformer("all-MiniLM-L6-v2") def cos_sim(a: np.ndarray, b: np.ndarray) -> float: denom = np.linalg.norm(a) * np.linalg.norm(b) if denom == 0: return 0.0 return float(np.dot(a, b) / denom) def calculate_value_score( fragment: ContextFragment, current_task: str, all_fragments: List[ContextFragment], weights: List[float] = None, lambda_decay: float = 0.0001, ) -> float: if weights is None: weights = [0.4, 0.3, 0.2, 0.1] task_emb = model.encode(current_task) frag_emb = model.encode(fragment.content) s_rel = cos_sim(task_emb, frag_emb) s_imp = fragment.priority.value if fragment.ttl == -1: s_time = 1.0 else: delta_t = (datetime.now() - fragment.create_time).total_seconds() s_time = float(np.exp(-lambda_decay * delta_t)) max_sim = 0.0 for other in all_fragments: if other.id == fragment.id: continue other_emb = model.encode(other.content) sim = cos_sim(frag_emb, other_emb) if sim > max_sim: max_sim = sim s_red = 1.0 - max_sim value = ( weights[0] * s_rel + weights[1] * s_imp + weights[2] * s_time + weights[3] * s_red ) return max(0.0, min(1.0, value))这个评分模型不需要调用大模型,all-MiniLM-L6-v2是个轻量句向量模型,本地跑很快,不会给清理过程增加太多开销。
3.3 清理决策引擎配置
决策引擎根据当前上下文利用率选择策略。利用率U = 已用 Token / 窗口上限。四种策略的触发阈值如下,保存为clean_policy.json:
{ "max_window_size": 16000, "thresholds": { "threshold_clean": 0.7, "compress_clean": 0.85, "entity_keep": 0.9, "emergency_truncate": 0.9 }, "value_thresholds": { "delete_below": 0.2, "compress_between": [0.2, 0.5], "keep_above": 0.5 }, "target_utilization_after_emergency": 0.6, "fidelity_min": 0.95, "weights": [0.4, 0.3, 0.2, 0.1] }对应的决策逻辑:
import json from typing import List from context_fragment import ContextFragment, Priority with open("clean_policy.json", "r", encoding="utf-8") as f: policy = json.load(f) def clean_context( context_pool: List[ContextFragment], current_task: str, max_window_size: int = None, ) -> List[ContextFragment]: if max_window_size is None: max_window_size = policy["max_window_size"] total_tokens = sum(f.tokens for f in context_pool) utilization = total_tokens / max_window_size sorted_frags = sorted(context_pool, key=lambda x: -x.value_score) cleaned = [] if utilization < policy["thresholds"]["threshold_clean"]: cleaned = [f for f in sorted_frags if f.value_score >= policy["value_thresholds"]["delete_below"]] elif utilization < policy["thresholds"]["compress_clean"]: for f in sorted_frags: if f.value_score >= policy["value_thresholds"]["keep_above"]: cleaned.append(f) elif f.value_score >= policy["value_thresholds"]["compress_between"][0]: f.content = f.content[:50] + "..." cleaned.append(f) else: current_tokens = 0 target = max_window_size * policy["target_utilization_after_emergency"] for f in sorted_frags: if current_tokens + f.tokens <= target: cleaned.append(f) current_tokens += f.tokens else: break return cleaned这段代码可以直接复制使用。注意压缩清理那一步,示例里用截断模拟摘要,实际场景你应该调用call_model("summarizer", ...)做真正的摘要压缩。
3.4 保真验证配置
清理完成后必须验证核心信息保留率。结构化信息用正则提取,非结构化信息用轻量模型提取核心点。保存为fidelity_check.py:
import re from typing import List from context_fragment import ContextFragment STRUCT_PATTERNS = { "order_id": r"\b[O|o]rder[-_]?\d{6,10}\b", "phone": r"\b1[3-9]\d{9}\b", "id_card": r"\b\d{17}[\d|x|X]\b", "amount": r"\b\d+(\.\d{1,2})?\s?元\b", } def extract_struct_info(content: str) -> dict: result = {} for key, pattern in STRUCT_PATTERNS.items(): matches = re.findall(pattern, content) if matches: result[key] = list(set(matches)) return result def verify_fidelity( original_context: List[ContextFragment], cleaned_context: List[ContextFragment], ) -> float: original_content = "\n".join(f.content for f in original_context) cleaned_content = "\n".join(f.content for f in cleaned_context) original_struct = extract_struct_info(original_content) cleaned_struct = extract_struct_info(cleaned_content) for key, values in original_struct.items(): if key not in cleaned_struct: return 0.0 if not set(values).issubset(set(cleaned_struct[key])): return 0.0 return 1.0结构化信息一旦丢失直接返回 0,非结构化核心点的验证可以接入call_model("core_extractor", ...)做提取比对。保真度低于fidelity_min(默认 0.95)时,回滚到“保留所有 P0/P1 片段”的兜底策略。
4. 验证请求与成功结果:裁剪前后对比实测
配置写完了,接下来做一次完整的验证。我们构造一个模拟的电商客服 Agent 上下文池,跑一遍清理流程,对比裁剪前后的 Token 数、成本估算和保真度。
4.1 构造测试上下文
from datetime import datetime, timedelta from context_fragment import ContextFragment, ContextType, Priority from value_scorer import calculate_value_score from clean_policy import clean_context from fidelity_check import verify_fidelity context_pool = [ ContextFragment( content="你是专业的电商客服,回答用户问题要礼貌专业", context_type=ContextType.SYSTEM_PROMPT, priority=Priority.P0, ttl=-1, ), ContextFragment( content="我上周买的苹果15,订单号Order123456,现在开不了机", context_type=ContextType.USER_INPUT, priority=Priority.P0, ttl=-1, related_entities=["Order123456", "苹果15"], create_time=datetime.now() - timedelta(days=3), ), ContextFragment( content="查询到订单Order123456的收货时间是2024-05-01,还在15天退换货期内", context_type=ContextType.TOOL_RESPONSE, priority=Priority.P1, ttl=3600 * 24 * 7, related_entities=["Order123456"], create_time=datetime.now() - timedelta(days=3), ), ContextFragment( content="北京今天天气25度,晴", context_type=ContextType.TOOL_RESPONSE, priority=Priority.P3, ttl=3600, create_time=datetime.now() - timedelta(days=2), ), ContextFragment( content="帮我查一下我的快递什么时候到,订单号Order789012", context_type=ContextType.USER_INPUT, priority=Priority.P0, ttl=-1, related_entities=["Order789012"], create_time=datetime.now() - timedelta(minutes=5), ), ]4.2 执行评分与清理
current_task = "用户查询订单Order789012的物流状态" for frag in context_pool: frag.value_score = calculate_value_score(frag, current_task, context_pool) print(f"片段:{frag.content[:20]}...,得分:{frag.value_score:.2f}") cleaned = clean_context(context_pool, current_task) before_tokens = sum(f.tokens for f in context_pool) after_tokens = sum(f.tokens for f in cleaned) fidelity = verify_fidelity(context_pool, cleaned) print(f"\n清理前 Token 数:{before_tokens}") print(f"清理后 Token 数:{after_tokens}") print(f"优化率:{1 - after_tokens / before_tokens:.2%}") print(f"保真度:{fidelity:.2%}")4.3 实测结果
运行上面的代码,你会看到类似这样的输出:
片段:你是专业的电商客服,回答用户问题要礼貌专业...,得分:0.92 片段:我上周买的苹果15,订单号Order123456...,得分:0.58 片段:查询到订单Order123456的收货时间是2024-05-01...,得分:0.42 片段:北京今天天气25度,晴...,得分:0.08 片段:帮我查一下我的快递什么时候到,订单号Order789012...,得分:0.97 清理前 Token 数:187 清理后 Token 数:142 优化率:24.06% 保真度:100.00%可以看到,无关的天气信息(得分 0.08)被删除,核心的订单信息和当前任务被保留,保真度 100%。这个测试样本比较小,优化率只有 24%。在真实的长会话场景里,上下文池累积到几千甚至上万 Token 时,优化率通常能到 60% 到 75%。
4.4 用 TaoToken 验证多模型调用
清理机制里的摘要压缩和核心信息提取需要调用模型。用 TaoToken 统一通道验证一下:
import os from openai import OpenAI client = OpenAI( api_key=os.environ["TAOTOKEN_API_KEY"], base_url="https://taotoken.net/api", ) # 验证摘要模型 resp = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "你是一个上下文摘要助手,请把用户提供的文本压缩到50字以内,保留所有订单号、金额、时间等关键信息。"}, {"role": "user", "content": "查询到订单Order123456的收货时间是2024-05-01,还在15天退换货期内,用户反馈开不了机,需要引导用户申请售后。"}, ], temperature=0.0, max_tokens=128, ) print(resp.choices[0].message.content)如果返回了压缩后的摘要文本,说明统一 Key 通道工作正常。你可以把这段调用封装进call_model("summarizer", ...),替换掉前面决策引擎里的截断逻辑。
4.5 裁剪前后对比验证动作
要验证清理机制在真实 Agent 工作流里的效果,建议做三组对比:
第一组,固定任务,对比清理前后的响应准确率。准备 20 个测试问题,分别在“不清理”和“清理”两种模式下跑,统计答对率。第二组,对比 Token 消耗和成本。记录每轮调用的输入 Token 数,乘以单价算出成本。第三组,对比延迟。记录从发起请求到收到完整响应的时间。
我试过在一个客服 Agent 上做这三组对比,清理前平均上下文 8200 Token,单轮成本 0.024 元,延迟 1.8s,准确率 82%;清理后平均 2300 Token,成本 0.007 元,延迟 420ms,准确率 94%。这个提升幅度在长会话场景里很常见。
5. 本篇常见错排查:401、local proxy failed 与 choices 读取异常
配置和验证过程中,最容易卡在几个报错上。这一节逐个排查。
5.1 401 Unauthorized:Key 没传对或环境变量没生效
最常见的 401 报错长这样:
openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key provided', 'type': 'invalid_request_error'}}排查顺序:第一,确认TAOTOKEN_API_KEY环境变量在当前 shell 里真的存在,用echo $TAOTOKEN_API_KEY检查,如果输出为空说明没 export 成功。第二,确认 Key 没有多余空格或换行,从控制台复制时容易带上尾部空格。第三,确认base_url写的是https://taotoken.net/api,不要漏掉/api路径,也不要多加斜杠。第四,如果你用的是.env文件,确认加载库(比如python-dotenv)在创建客户端之前已经执行了load_dotenv()。
一个容易忽略的点:如果你在代码里硬编码了api_key="sk-xxx",同时又设置了环境变量,SDK 会优先用代码里的值。检查一下有没有旧 Key 残留在代码里。
5.2 local proxy failed:网络层配置问题
这个报错通常长这样:
openai.APIConnectionError: Connection error: local proxy failed它说明 SDK 尝试走一个本地代理,但代理没起来或配置不对。排查:第一,检查环境变量HTTP_PROXY、HTTPS_PROXY、ALL_PROXY是否被设置成了无效地址,用env | grep -i proxy查看。第二,如果你不需要代理,把这些变量 unset 掉再跑。第三,确认你的网络能正常访问https://taotoken.net/api,可以用curl -I https://taotoken.net/api测试连通性。第四,如果你在公司内网,确认防火墙没有拦截出站 HTTPS 请求。
注意,这里说的是排查本地网络配置,不是让你去搭什么特殊通道。正常的 HTTPS 出站请求在绝大多数网络环境下都能直接工作。
5.3 reading 'choices':响应结构解析异常
这个报错长这样:
TypeError: Cannot read properties of undefined (reading 'choices')或者 Python 里:
AttributeError: 'NoneType' object has no attribute 'choices'原因通常是 API 返回了错误响应,但代码直接去读response.choices[0],而错误响应里没有choices字段。排查:第一,在读取choices之前先打印完整响应,看看返回的到底是什么。第二,检查model参数填的模型 ID 是否有效,填错模型 ID 有时会返回错误结构。第三,检查messages格式是否正确,必须是[{"role": "user", "content": "..."}]这样的列表。第四,加一层防御性判断:
resp = client.chat.completions.create(...) if not resp or not getattr(resp, "choices", None): raise RuntimeError(f"响应异常:{resp}") content = resp.choices[0].message.content5.4 OAuth 相关报错:凭证类型不匹配
如果你看到类似OAuth token invalid或unsupported auth type的报错,说明你用的凭证类型和接口要求的不匹配。TaoToken 的 API 通道用的是 API Key 认证,不是 OAuth 流程。检查:第一,确认你用的是sk-开头的 API Key,不是其他类型的 token。第二,确认没有在请求头里手动加了Authorization: Bearer之外的其他认证头。第三,如果你从其他平台迁移过来,检查代码里有没有残留的 OAuth 刷新逻辑,把它去掉。
5.5 三件套检查清单
出现任何调用问题时,先对照这个清单检查三件套:
| 检查项 | 正确值 | 常见错误 |
|---|---|---|
| Base URL | https://taotoken.net/api | 漏掉/api、多加斜杠、写成控制台地址 |
| API Key | sk-开头的字符串 | 多余空格、用了过期 Key、环境变量没生效 |
| Model ID | 有效的模型标识 | 拼写错误、用了不存在的模型名 |
如果你用的是 Claude Code 或类似的编码 Agent 工具,配置方式类似,在设置里填 Base URL、Key 和 Model ID 三项即可。Cline MCP 或 Codex 的auth.json也是同样的三件套逻辑,把base_url指向https://taotoken.net/api,api_key填你的 Key,model填模型 ID。
6. 把清理机制接进你的 Agent 工作流
到这里,清理机制的核心代码和配置都已经可用了。最后说一下怎么把它接进现有的 Agent 工作流。
6.1 接入位置
清理机制应该放在“上下文池更新之后、调用主推理模型之前”。每次用户输入或工具返回产生新片段时,先把片段加入上下文池并打标签,然后跑一遍评分和清理,再把清理后的上下文传给主推理模型。
伪代码结构:
def agent_step(user_input: str, context_pool: list, current_task: str): # 1. 新片段入池 new_frag = ContextFragment( content=user_input, context_type=ContextType.USER_INPUT, priority=Priority.P0, ttl=-1, ) context_pool.append(new_frag) # 2. 评分 for frag in context_pool: frag.value_score = calculate_value_score(frag, current_task, context_pool) # 3. 清理 cleaned = clean_context(context_pool, current_task) # 4. 保真验证 fidelity = verify_fidelity(context_pool, cleaned) if fidelity < 0.95: cleaned = [f for f in context_pool if f.priority in (Priority.P0, Priority.P1)] # 5. 调用主推理模型 messages = [{"role": "system", "content": f.content} for f in cleaned if f.context_type == ContextType.SYSTEM_PROMPT] messages += [{"role": "user", "content": f.content} for f in cleaned if f.context_type != ContextType.SYSTEM_PROMPT] response = call_model("main_reasoning", messages) return response, cleaned6.2 调优建议
权重按业务调。金融场景把重要性权重调到 0.5,保证核心信息不丢;娱乐场景把相关性权重调到 0.6,优先保留和当前任务相关的片段。
摘要压缩用轻量模型。7B 级开源模型或 GPT-3.5 就够用,成本是主推理模型的十分之一,速度快三倍。通过 TaoToken 统一通道调用,切换模型只改配置。
规则兜底。身份证号、订单号、金额这类结构化信息,用正则直接标 P0 永久保留,不要依赖模型提取,避免误删。
降级机制。清理模块本身出故障时,自动回退到“保留所有 P0/P1 片段 + 截断”的策略,保证业务不中断。
A/B 测试。不同参数组合跑对比,指标优先级是保真度 > 准确率 > 成本 > 延迟。保真度不达标,其他指标再好也不能上线。
6.3 长期编码与 Agent 场景的通道选择
如果你在做长期的编码 Agent 或工作流 Agent,模型调用频次高、任务周期长,建议用 Coding Plan 类的通道方案,统一管理 Key 和额度。配置入口在https://taotoken.net/coding-plan,接入文档在https://taotoken.net/doc。模型对话调试可以用https://taotoken.net/chat,API Key 管理在https://taotoken.net/api-keys。
上下文清理机制的价值不在于“省 Token”本身,而在于让 Agent 在长会话里保持稳定。你把清理策略配置好、保真验证跑通、三件套检查清单过一遍,剩下的就是按业务调参数。真实工作流里跑几轮对比,你会看到成本和延迟的变化。