1. 多 Agent 工具服务为什么总在 Key 上翻车
AI Agent 落地到容器环境后,最先暴露的往往不是模型能力问题,而是 Key 管理问题。LangChain 的 Agent 要调模型,AutoGen 的 GroupChat 要调模型,Agent Mesh 里每个工具服务在需要做语义路由或结果摘要时也要调模型。如果每个容器都挂一份自己的OPENAI_API_KEY,你会遇到三个典型症状:一是密钥散落在十几份 YAML 和.env里,轮换一次要改半天;二是某个工具服务被反编译或日志打印,Key 直接泄露;三是不同服务走不同出口,计费和限流完全对不上账。
我试过在一个 8 服务的 Agent 编排里逐个改 Key,改到第三个就放弃了。后来统一成一条 Key 通道:所有容器只认一个环境变量,指向同一个兼容 OpenAI 协议的入口,模型名、路由、额度都在通道侧管理。这篇就按这个思路,把 LangChain、AutoGen、Agent Mesh 三类 Agent 工具服务容器化,并给出 Docker Compose 骨架、settings.json与config.toml的配置片段,最后演示容器起来后怎么验证鉴权和路由真的生效。
适合谁看:已经在写 Agent、准备把 Agent 服务塞进容器或 K8s、被多份 Key 折磨过的后端和算法同学。读完你能拿到一套可直接跑的编排骨架,而不是又一篇概念科普。
2. TaoToken 统一 Key 通道:前置准备
核心思路是把模型调用收敛成一个 OpenAI 兼容端点。TaoToken 提供的就是这样一个入口,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。所有 Agent 容器只需要两个变量:OPENAI_API_KEY和OPENAI_BASE_URL,前者是你在控制台生成的 Key,后者固定指向上面这个 API 地址。
先去控制台建 Key,入口在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,生成后复制保存。Key 的权限和额度管理都在这一层,容器侧不再关心具体模型供应商。如果你要确认某个模型名是否可用,可以直接在模型对话页试一条请求:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。
注意:Key 只放在编排的环境变量或 Secret 里,不要写进镜像、不要提交到 Git。容器日志里也不要打印
OPENAI_API_KEY。
对长期跑编码类 Agent 的场景,可以了解下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它更适合持续性的 Agent 编码任务。接入细节和参数说明看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
3. Docker Compose 骨架与三类 Agent 配置
先给一份能跑起来的 Compose 骨架。它包含三个服务:langchain-agent、autogen-manager、agent-mesh-router,全部通过同一个OPENAI_BASE_URL和OPENAI_API_KEY访问模型通道。
# docker-compose.yml version: "3.9" x-agent-env: &agent-env OPENAI_API_KEY: ${TAOTOKEN_API_KEY} OPENAI_BASE_URL: https://taotoken.net/api DEFAULT_MODEL: gpt-4o-mini services: langchain-agent: build: ./langchain-agent environment: <<: *agent-env AGENT_ROLE: "tool-executor" ports: - "8001:8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 15s timeout: 5s retries: 3 autogen-manager: build: ./autogen-manager environment: <<: *agent-env AGENT_ROLE: "group-manager" ports: - "8002:8000" depends_on: - langchain-agent agent-mesh-router: build: ./agent-mesh-router environment: <<: *agent-env AGENT_ROLE: "mesh-router" UPSTREAM_AGENTS: "http://langchain-agent:8000,http://autogen-manager:8000" ports: - "8003:8000" depends_on: - langchain-agent - autogen-managerTAOTOKEN_API_KEY从宿主机.env注入,Compose 只做变量透传,不落盘到镜像。这样轮换 Key 时只改一处。
3.1 LangChain 侧 settings.json 配置
LangChain 的 Agent 服务里,把模型初始化收敛到一个工厂函数,读环境变量。同时用一份settings.json描述工具与模型映射,避免硬编码。
{ "llm": { "provider": "openai-compatible", "base_url_env": "OPENAI_BASE_URL", "api_key_env": "OPENAI_API_KEY", "default_model": "gpt-4o-mini", "temperature": 0, "max_iterations": 8, "max_execution_time": 60 }, "tools": [ { "name": "search_repos", "endpoint": "http://mcp-github:8080/dispatch" }, { "name": "query_db", "endpoint": "http://db-query:8080/query" } ] }对应的加载代码:
# langchain-agent/app.py import json, os from langchain_openai import ChatOpenAI from langchain.agents import AgentExecutor, create_openai_tools_agent with open("settings.json") as f: cfg = json.load(f) llm = ChatOpenAI( model=cfg["llm"]["default_model"], base_url=os.environ["OPENAI_BASE_URL"], api_key=os.environ["OPENAI_API_KEY"], temperature=cfg["llm"]["temperature"], ) agent_executor = AgentExecutor( agent=agent, tools=tools, max_iterations=cfg["llm"]["max_iterations"], max_execution_time=cfg["llm"]["max_execution_time"], )base_url和api_key都从环境变量取,settings.json只留模型名和策略参数。这样同一份镜像可以在不同环境复用,Key 由编排层注入。
3.2 AutoGen 侧 config.toml 配置
AutoGen 支持从config.toml读取模型配置,正好用来对接统一通道。
# autogen-manager/config.toml [llm] provider = "openai" base_url = "https://taotoken.net/api" api_key_env = "OPENAI_API_KEY" model = "gpt-4o-mini" temperature = 0 timeout = 60 [group_chat] max_round = 15 speaker_selection = "round_robin"加载方式:
# autogen-manager/main.py import os, tomllib, autogen with open("config.toml", "rb") as f: cfg = tomllib.load(f) llm_cfg = cfg["llm"] config_list = [{ "model": llm_cfg["model"], "base_url": llm_cfg["base_url"], "api_key": os.environ[llm_cfg["api_key_env"]], "temperature": llm_cfg["temperature"], }] manager = autogen.GroupChatManager( groupchat=autogen.GroupChat(agents=agents, messages=[], max_round=cfg["group_chat"]["max_round"]), llm_config={"config_list": config_list}, )base_url写死在config.toml里没问题,因为它不是敏感信息;api_key仍然走环境变量。这样 AutoGen 的每个 Agent 共享同一份通道配置。
3.3 Agent Mesh 路由侧配置
Agent Mesh 这一层负责把请求分发到具体 Agent,并在需要时做结果摘要。它同样只认统一通道。
# agent-mesh-router/main.py import os, httpx from fastapi import FastAPI, Request app = FastAPI() UPSTREAM = os.environ["UPSTREAM_AGENTS"].split(",") BASE_URL = os.environ["OPENAI_BASE_URL"] API_KEY = os.environ["OPENAI_API_KEY"] @app.post("/route") async def route(req: Request): body = await req.json() target = pick_agent(body["task_type"]) async with httpx.AsyncClient() as client: resp = await client.post(target, json=body, timeout=60) return resp.json() async def summarize(text: str) -> str: async with httpx.AsyncClient() as client: resp = await client.post( f"{BASE_URL}/chat/completions", headers={"Authorization": f"Bearer {API_KEY}"}, json={"model": "gpt-4o-mini", "messages": [{"role": "user", "content": text}]}, ) return resp.json()["choices"][0]["message"]["content"]路由层和摘要层共用同一 Key,不需要额外配置。
4. 启动容器并验证鉴权与路由
配置齐了,直接起。
export TAOTOKEN_API_KEY="你的Key" docker compose up -d --build docker compose ps三个服务都应该是healthy或running。先验证鉴权是否生效,直接打 LangChain Agent 的健康检查加一次模型调用:
curl -s http://localhost:8001/health curl -s -X POST http://localhost:8001/invoke \ -H "Content-Type: application/json" \ -d '{"input": "用一句话说明什么是 Agent"}'如果 Key 或base_url配错,这里会返回 401 或连接错误。正常情况你会拿到一段模型输出。再验证 AutoGen 的多 Agent 协作:
curl -s -X POST http://localhost:8002/chat \ -H "Content-Type: application/json" \ -d '{"message": "让研究员和程序员协作输出一个快速排序示例"}'最后验证 Mesh 路由是否把请求分发到了正确的上游:
curl -s -X POST http://localhost:8003/route \ -H "Content-Type: application/json" \ -d '{"task_type": "code", "input": "写一个二分查找"}'成功的结果是:路由层返回的响应里带有上游 Agent 的处理结果,且三个服务的日志中都能看到对https://taotoken.net/api的请求记录,而不是各自打向不同域名。这说明统一 Key 通道生效了。
提示:验证阶段可以把
DEFAULT_MODEL换成更便宜的模型,确认链路通了再切回目标模型,避免调试期浪费额度。
5. 本篇常见错排查
报错一:401 Unauthorized。九成是OPENAI_API_KEY没注入进容器。用docker compose exec langchain-agent env | grep OPENAI确认变量存在,再检查宿主机.env是否被 Compose 读取。注意 Compose 的变量替换发生在宿主机侧,容器内看不到TAOTOKEN_API_KEY这个名字,只有OPENAI_API_KEY。
报错二:Connection refused 或超时。检查OPENAI_BASE_URL是否写成了带路径的完整地址。基址应该是https://taotoken.net/api,SDK 会自己拼/chat/completions。如果你手动拼了/v1,可能重复。
报错三:模型名不存在。不同通道支持的模型名不完全一致。先在模型对话页确认可用模型,再写进settings.json和config.toml。报错通常是 404 或model not found。
报错四:AutoGen 起不来,提示 config 解析失败。tomllib是 Python 3.11 才进标准库的。如果你的基础镜像是 3.10,需要装tomli并改导入。或者直接把config.toml换成 JSON,减少依赖。
报错五:Mesh 路由 502。多半是UPSTREAM_AGENTS里的服务名解析不到。Compose 网络内用服务名互访,确认depends_on和端口对得上。K8s 里则要用 Service 名加命名空间。
报错六:容器日志里出现 Key 明文。检查你的代码有没有在异常分支打印os.environ。统一通道的好处是 Key 只有一份,但一旦泄露影响面也更大,日志脱敏要做。
6. 把 Key 通道固定下来之后
统一 Key 通道真正的价值不在省事,而在可观测。所有 Agent 的模型调用都经过同一个入口,你可以在通道侧看到哪个 Agent 在烧额度、哪个工具服务在疯狂重试。容器化只是第一步,把 Key 收敛成一条通道,后面做限流、做审计、做成本分摊才有抓手。
如果你还在逐个服务配 Key,建议先按这篇的 Compose 骨架把三个服务跑通,确认鉴权和路由都对了,再往 K8s 迁移。接入参数和更多配置示例看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ;Key 管理在控制台:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。编码类 Agent 长期跑的话,Coding Plan 会更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。