1. 多 Agent 协作 Token 膨胀到底出在哪
如果你正在用 CrewAI 或者 LangGraph 搭多智能体系统,大概率遇到过这个现象:单 Agent 跑一个任务消耗 2k Token,换成 planner / executor / reviewer 三个角色协作,同样的任务直接飙到 8k 甚至 12k。这不是错觉,CrewAI 官方文档在「预算与成本控制」一节里就明确写过,多 Agent 协作会产生 3~5 倍的 Token 膨胀。问题在于,很多人第一反应是去优化 Prompt、砍上下文,却忽略了一个更底层的原因——每个 Agent 各自持有一个模型客户端,各自连不同的厂商端点,成本边界和调用边界从一开始就是散的。
我试过在一个三节点的 StateGraph 里,planner 走某厂商的强模型、executor 走另一个厂商的小模型、reviewer 又换一家,结果就是:三个 API Key、三套计费口径、三种限流策略,LangSmith 的 trace 里能看到调用成功,但你根本说不清哪个角色在持续烧 Token。更麻烦的是,当你想做「模型分级路由」——Planner 用强模型、Executor 用小模型——你得在三个控制台之间来回切换,改一个 base_url 就要重新申请一次 Key。
这篇就是对着「多 Agent 协作 Token 膨胀」这个现象来改配置的。核心动作只有一个:把每个 Agent 模型客户端的 base_url 统一指向同一个入口,Key 也只用一个。编排、状态流转、条件路由仍然由 CrewAI / LangGraph 自己完成,TaoToken 只负责提供 Key 和 Base URL。改完之后,你可以在 trace 里逐个节点核对调用,找出到底是哪个角色在膨胀,再补上单任务预算和超限阻断。
适合谁看:已经跑通单 Agent、正在往多 Agent 协作迁移,或者已经被 Token 账单吓到过的开发者。不需要你重写编排逻辑,只需要改几行客户端初始化代码。
2. 前置准备:一个 Key 管住所有 Agent 的模型入口
在动手改 base_url 之前,先把入口统一掉。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 注册账号,然后在控制台里创建一个 API Key。这个 Key 就是你后面所有 Agent 共用的凭证,不用再为 planner、executor、reviewer 分别去不同厂商申请。
创建 Key 的入口在控制台的 API Keys 页面,直接访问 https://taotoken.net/console 就能看到。生成之后先复制保存,后面配置环境变量要用。
这里要强调一个容易踩的坑:Base URL 填 https://taotoken.net/api,不要带 /v1。很多 OpenAI 兼容客户端默认会自己在后面拼 /v1/chat/completions,如果你手动写成 https://taotoken.net/api/v1,最终请求路径就会变成 /api/v1/v1/chat/completions,直接 404。这一点在接入文档里有说明,地址是 https://taotoken.net/doc ,配置前建议扫一眼。
环境变量建议这样组织,把 Key 和 Base URL 分开存,方便后面在多个 Agent 之间复用:
export TAOTOKEN_API_KEY="sk-你的Key"
export TAOTOKEN_BASE_URL="https://taotoken.net/api"
如果你用的是 .env 文件配合 python-dotenv,写法一样,只是去掉 export。这样做的目的是:后面无论 CrewAI 的 Agent 还是 LangGraph 的节点,初始化模型客户端时都从这两个变量读,改一处就全局生效。
模型分级路由也在同一个入口下调整。你不需要为「Planner 走强模型、Executor 走小模型」去开两个账号,只需要在创建客户端时传不同的 model 参数即可。比如 planner 用 gpt-4o 这类强模型,executor 用 gpt-4o-mini 这类小模型,两者共用同一个 base_url 和同一个 Key。这样成本口径统一,trace 里也能按 model 字段区分。
3. 可复制配置:CrewAI 与 LangGraph 双份改法
先看 CrewAI。CrewAI 底层用的是 LiteLLM 做模型调用,所以最干净的方式是通过环境变量让 LiteLLM 走统一入口,而不是在每个 Agent 里硬编码。你可以在 Crew 启动前设置:
import os
from crewai import Agent, Task, Crew, Process
os.environ["OPENAI_API_KEY"] = os.environ["TAOTOKEN_API_KEY"]
os.environ["OPENAI_API_BASE"] = os.environ["TAOTOKEN_BASE_URL"]
planner = Agent(
role="Planner",
goal="拆解用户任务为可执行步骤",
backstory="你负责规划,不直接执行",
llm="gpt-4o",
verbose=True,
)
executor = Agent(
role="Executor",
goal="按计划执行具体步骤",
backstory="你负责执行,遇到问题上报",
llm="gpt-4o-mini",
verbose=True,
)
reviewer = Agent(
role="Reviewer",
goal="校验执行结果是否符合预期",
backstory="你负责质量把关",
llm="gpt-4o-mini",
verbose=True,
)
crew = Crew(
agents=[planner, executor, reviewer],
tasks=[...],
process=Process.sequential,
verbose=True,
)
关键点在于 OPENAI_API_BASE 这个变量,LiteLLM 会读取它作为所有 OpenAI 兼容调用的默认端点。三个 Agent 虽然 llm 不同,但都走同一个 base_url,Key 也只有一个。这样 Token 膨胀的账就集中在一个地方,不会散落到三个厂商。
再看 LangGraph。原文 4.1 用 StateGraph 把 planner / executor / reviewer 三个节点接进同一张图,每个节点各自持有模型客户端。改法是把客户端初始化抽出来,统一从环境变量读:
import os
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from typing import TypedDict
class TaskState(TypedDict):
task: str
plan: list
results: dict
done: bool
def build_llm(model: str, temperature: float = 0.2):
return ChatOpenAI(
model=model,
temperature=temperature,
api_key=os.environ["TAOTOKEN_API_KEY"],
base_url=os.environ["TAOTOKEN_BASE_URL"],
)
planner_llm = build_llm("gpt-4o")
executor_llm = build_llm("gpt-4o-mini")
reviewer_llm = build_llm("gpt-4o-mini")
def planner_agent(state: TaskState):
resp = planner_llm.invoke(f"拆解任务:{state['task']}")
return {"plan": resp.content.split("\n")}
def executor_agent(state: TaskState):
resp = executor_llm.invoke(f"执行计划:{state['plan']}")
return {"results": {"output": resp.content}}
def reviewer_agent(state: TaskState):
resp = reviewer_llm.invoke(f"校验结果:{state['results']}")
return {"done": "通过" in resp.content}
workflow = StateGraph(TaskState)
workflow.add_node("planner", planner_agent)
workflow.add_node("executor", executor_agent)
workflow.add_node("reviewer", reviewer_agent)
workflow.add_edge("planner", "executor")
workflow.add_edge("executor", "reviewer")
workflow.add_conditional_edges(
"reviewer",
lambda state: "done" if state["done"] else "retry",
{"done": END, "retry": "executor"},
)
workflow.set_entry_point("planner")
app = workflow.compile()
result = app.invoke({"task": "分析Q2销售数据"})
注意 base_url 传的是 https://taotoken.net/api,不带 /v1。ChatOpenAI 内部会自己拼 /chat/completions。如果你用的是其他 LangChain 兼容客户端,参数名可能是 openai_api_base,但值是一样的。
模型分级路由在这里体现得很清楚:planner 用强模型负责规划,executor 和 reviewer 用小模型负责执行和校验。三个客户端共用同一个 Key 和 base_url,但 model 字段不同。这样你在 trace 里既能按节点看调用,也能按 model 看成本分布。
4. 验证请求:从 trace 里逐个节点核对
配置改完,先别急着跑完整任务,用一个最小请求验证链路通不通。最直接的方式是用 curl 打一次 chat completions:
curl https://taotoken.net/api/chat/completions \
-H "Authorization: Bearer $TAOTOKEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role": "user", "content": "回复 OK"}]
}'
如果返回里有正常的 choices 字段,说明 Key 和 Base URL 都没问题。如果返回 401,检查 Key 是否复制完整;如果返回 404,八成是 base_url 多写了 /v1。
链路通了之后,跑一次完整的 LangGraph 任务,然后打开 LangSmith 或 LangFuse 的 trace 面板。你要重点看三件事:
第一,每个节点的调用是否成功。planner、executor、reviewer 三个节点应该各自有一条 LLM 调用记录,状态都是 success。如果某个节点报错,先看它的 model 字段是不是写错了。
第二,哪个角色在持续消耗 Token。在 trace 里按节点展开,看每个节点的 prompt tokens 和 completion tokens。多 Agent 协作的膨胀往往集中在某一个角色上——常见的是 reviewer 因为反复校验、或者 executor 因为重试循环,导致 Token 远超预期。
第三,重试路径是否被触发。LangGraph 的条件路由里,reviewer 不通过会回到 executor 重试。如果 trace 里看到 executor 被调用了多次,说明校验逻辑太严或者 executor 输出质量不够,这时候要么调 reviewer 的判定 Prompt,要么给 executor 换更强的模型。
实测下来,把三个 Agent 的 base_url 统一之后,最直观的变化是成本口径清晰了。以前三个厂商三份账单,现在一个入口,按 model 字段就能拆出 planner 和 executor 各自的消耗占比。原文提到的「单任务预算」和「超限阻断」也有了落地基础——你可以在每个节点调用前检查累计 Token,超过阈值就抛异常终止,而不是等账单出来才发现。
5. 本篇常见错排查
报错一:404 Not Found,路径里出现两个 /v1
这是最高频的坑。原因就是 base_url 写成了 https://taotoken.net/api/v1,而客户端又自动拼了 /v1/chat/completions。解决方法是把 base_url 改回 https://taotoken.net/api,不带 /v1。CrewAI 的 LiteLLM 和 LangChain 的 ChatOpenAI 都是这个规则。
报错二:401 Unauthorized,Key 无效
先确认环境变量有没有真正加载。在 Python 里打印 os.environ.get("TAOTOKEN_API_KEY") 看是不是 None。如果是 None,说明 .env 没被读取,或者 export 的 shell 和跑代码的 shell 不是同一个。另外检查 Key 有没有多余空格,复制的时候容易带上换行。
报错三:某个 Agent 调用成功,另一个失败
如果 planner 成功但 executor 失败,先看两个 Agent 的 model 字段。有些模型名在不同入口下的可用性不一样,确认你填的 model 是当前入口支持的。另外检查是不是某个 Agent 硬编码了旧的 base_url,没有走统一的环境变量。CrewAI 里如果 Agent 初始化时显式传了 llm 对象而不是字符串,那个对象可能还带着旧端点。
报错四:Token 消耗没有下降,反而更高
统一入口本身不会自动降低 Token 消耗,它只是让消耗可见、可管。如果改完之后发现总 Token 没降,去 trace 里看是不是重试循环变多了。常见原因是 reviewer 的判定太严格,导致 executor 反复重跑。这时候要调的是校验逻辑,不是 base_url。另外确认模型分级路由有没有生效——如果 executor 还在用强模型,成本自然下不来。
报错五:LangSmith 里看不到 trace
LangSmith 需要在环境变量里配 LANGCHAIN_TRACING_V2=true 和 LANGCHAIN_API_KEY。这两个和 TaoToken 的 Key 是两回事,别混。如果 trace 一直不出现,先确认这两个变量有没有设,再看网络能不能通到 LangSmith 的端点。
6. 把预算和阻断补上
链路跑通、trace 能看之后,最后一步是把原文说的单任务预算和超限阻断落地。思路很简单:在 StateGraph 的状态里加一个 token_used 字段,每次节点调用后累加,超过阈值就路由到 END 或者抛异常。
class TaskState(TypedDict):
task: str
plan: list
results: dict
done: bool
token_used: int
TOKEN_BUDGET = 20000
def planner_agent(state: TaskState):
resp = planner_llm.invoke(f"拆解任务:{state['task']}")
used = state.get("token_used", 0) + resp.usage_metadata["total_tokens"]
if used > TOKEN_BUDGET:
raise RuntimeError(f"Token 预算超限:{used}")
return {"plan": resp.content.split("\n"), "token_used": used}
每个节点都做同样的累加和检查,这样无论哪个角色在膨胀,都会在超限时立刻中断,而不是跑完整个流程才发现账单爆了。配合 trace 里的节点级消耗,你就能定位到是 planner 规划太啰嗦、还是 executor 重试太多、还是 reviewer 校验太严,然后针对性优化。
如果你后面要长期跑多 Agent 的编码或 Agent 任务,可以看一下 Coding Plan 这个入口 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,它更适合持续性的编码场景。日常调试模型调用是否正常,用模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 快速验证就行。Key 的管理和新建都在 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,接入细节以文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 为准。Claude Code 相关的接入配置在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite 有单独说明。
改完这一轮,你手里应该有一个统一的模型入口、一份按节点可查的 trace、以及一个能自动阻断的超限逻辑。多 Agent 协作的 Token 膨胀不会凭空消失,但至少从「不知道为什么贵」变成了「知道贵在哪、能管住」。




被折叠的 条评论
为什么被折叠?



