Agent Harness 的模型调用层,Base URL 填 TaoToken
Agent Harness 的模型调用层常被 Demo 写成三行直连代码换台机器就失联。TaoToken 只补这一层通道打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 注册并创建 Key把 Harness 里的 Base URL 填成 https://taotoken.net/apiKey 走环境变量注入。一轮 Turn 里模型被 Loop 反复调用、多工具来回递纸条这种节奏下最怕的不是模型不够聪明而是通道写死在 client 初始化那一行——上下文装配、Prompt 编译、Tool Schema、Permission Check 都设计得有模有样结果第一个请求就 401或者同事 clone 下来连域名都不认识。下面按 Harness 的层次往下拆重点只落在两处模型调用层的 client 初始化以及 Loop Controller 到底怎么把每一轮「模型请求 → 工具调用 → Observation」串起来。1. 一轮 Turn 里 Loop 调了七次模型Demo 的 client 在哪一步断掉1.1 SRE RCA Agent 的一轮 Turn模型只是递纸条的人拿一个只读诊断场景举例。值班同学在对话里说「昨天 22:10 到 22:40 订单库连接数飙高帮我定位原因。」Harness 接到这句话之后做的事和模型本身关系不大上下文装配层去拉拓扑、SLO、最近告警、变更单决定哪些进 messages、哪些只留摘要Prompt 编译层把系统提示、角色约束、输出格式拼成最终请求工具边界层把「查告警」「查慢查询样本」「查变更单」这几个只读工具整理成 Tool Schema 递进去然后是模型调用层发请求。模型返回的第一个动作通常不是答案而是一次工具调用——它只是递纸条的人纸条上写着「我需要看这段时间的活跃会话采样」。Loop Controller 收到这张纸条交给 Permission Check 判断这个工具是否属于只读白名单确认只读本地执行拿到原始结果Observation 清洗层把几万行采样裁成几百 token 的摘要塞回 messages再发一次模型请求。如此往复一轮 Turn 里发七八次模型请求是常态中间还夹着两三次「危险动作需人工审批」的挂起。真正决定什么时候停、哪个动作要审批、哪条 Observation 该丢的全是 Harness 自己的逻辑。模型在整条流水线里只负责一件事拿着当前上下文决定下一张纸条写什么。1.2 直连 client 的三种典型断法第一种是换环境断。base_url 直接写在OpenAI(...)那一行指向一台内网机器。同事把仓库 clone 下来第一次运行就卡在连接超时翻遍代码才发现地址写死在一个和业务逻辑混在一起的模块里。第二种是换模型断。模型 ID 硬编码成字符串常量某天上游把那个 ID 下线整条 Loop 在第一步就抛异常。更麻烦的是这个异常发生在 Loop 内部外层只看到一个「任务失败」Trace 里连「模型请求失败」这条记录都没落上。第三种是 Key 进仓库断。api_key 写在初始化那一行某次提交顺手带上去了。轮换 Key 的时候要改代码、发版、重新部署而 Harness 里其它七层根本不需要跟着动。这三种断法的共同点都是把「通道」和「Harness 逻辑」焊死在了一起。2. 拆到模型调用层Harness 里只有这一段跟供应商有关2.1 上下文装配、Prompt 编译、Tool Schema 都不该知道 Key给 Harness 做分层练习一个简单的判断标准是把 Key 全部涂黑剩下的代码还能不能读懂。上下文装配层关心的是「这次要不要把上一次的 Observation 摘要带上」Prompt 编译层关心模板里的占位符怎么填Tool Schema 关心参数类型和必填项Permission Check 关心动作风险等级。这些层没有一层需要知道 Key 长什么样、请求发到哪个域名。Key 和 Base URL 只在最底下的模型调用层出现一次而且是以配置的形式出现。分层做得越干净后面换通道的成本就越低——只动一个环境变量不动任何一行业务逻辑。2.2 Base URL 与模型 ID 必须可注入Harness 通常要跑在好几个地方你本地的笔记本、CI 里跑回归、预发容器、线上 Pod。这些环境唯一的差别就是模型通道的两个值请求发到哪里用哪个模型。既然只有这两处不同它们就不该是常量而应该是环境变量。这里有一个很容易踩的细节填进工具和代码的 Base URL 是https://taotoken.net/api末尾不要加/v1也不要附加任何查询参数。很多 SDK 自己会拼接路径你多写一段就会变成https://taotoken.net/api/v1/chat/completions这种不存在的路径表现出来是 404而不是 401排查时容易误判成模型名写错。2.3 创建 Key打开官网拿到 YOUR_API_KEY动手之前先把凭证备好。打开 TaoToken 官网注册并登录进控制台创建一把 API Key复制出来。本文所有示例里这把 Key 一律用占位符YOUR_API_KEY代替不要把它贴进任何提交到仓库的文件。顺手在模型广场确认一下可用的模型 ID——写配置时照抄列表里的值不要凭印象编一个带日期后缀的名字。模型 ID 属于会变的东西以模型广场当时列表为准别抄几个月前的博客。3. 改 harness/model_client.py把 client 初始化换成环境变量注入3.1 先看要替换掉的那段直连代码大部分 Demo 的模型调用层长得大概是这样# 反面示例初始化、凭证、模型名全糊在一行里 from openai import OpenAI client OpenAI( base_urlhttps://某供应商域名/v1, api_key写死在源码里的字符串, ) resp client.chat.completions.create( model某个固定模型名, messagesmessages, )问题不在于它能不能跑——它能跑而且跑得挺好。问题在于run_turn里的业务逻辑和通道配置耦合在一起你没法在不改业务代码的前提下换环境也没法在不重新发版的前提下轮换 Key。3.2 换成一个只负责建 client 的小模块把初始化单独抽出来只做一件事# harness/model_client.py import os from openai import OpenAI def _base_url() - str: url os.environ[MODEL_BASE_URL].rstrip(/) if not url.endswith(/api): raise RuntimeError( MODEL_BASE_URL 应指向 https://taotoken.net/api末尾不要带 /v1 ) return url def build_client() - OpenAI: return OpenAI( base_url_base_url(), api_keyos.environ[MODEL_API_KEY], timeout60.0, max_retries2, ) def model_id() - str: return os.environ[MODEL_ID]这个模块只认环境变量不认常量。timeout和max_retries建议显式写出来Harness 的 Loop 本身会重试通道层再叠一层无上限重试出问题时就是重试风暴。3.3 .env 与启动脚本Key 落在进程环境里本地开发用一个不提交的 env 文件就够了# .env.local务必加进 .gitignore export MODEL_BASE_URLhttps://taotoken.net/api export MODEL_API_KEYYOUR_API_KEY export MODEL_ID从模型广场复制的模型 IDYOUR_API_KEY的位置换成你刚才在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 创建的那把 Key。容器部署时对应改成 Secret 挂载的环境变量别写进镜像层。Key 的使用边界要守住三条不进 Prompt不进 Tool Schema不进仓库。Tool Schema 是发给模型看的结构描述把 Key 塞进参数默认值等于把凭证写进每一次请求的正文里。3.4 顺手用 Claude Code 或 Codex 改这段代码时它们的 Base URL 同样填 TaoToken如果你打算用 Claude Code 帮忙做这次重构它自己的通道也要先配通。~/.claude/settings.json里加一段 env{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: YOUR_API_KEY, ANTHROPIC_MODEL: 模型广场里的模型 ID } }用 Codex 改的话是~/.codex/config.toml注意别把ANTHROPIC_*那套变量套到 Codex 上model 模型广场里的模型 ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat这两个工具在这里只是帮你重构 client 代码的编辑器它们不属于 Harness 的一部分。真正被改的是你自己项目里那个model_client.py。4. 让 Loop Controller 连跑三轮模型请求 → 工具调用 → Observation4.1 Loop 的骨架代码把每次模型请求写进 Trace改完 client 之后验证的重点不是「能不能发一条消息」而是「Loop 连跑几轮是否每一轮都成功」。骨架大概长这样def run_turn(state, tools, tracer): client build_client() for step in range(state.max_steps): resp client.chat.completions.create( modelmodel_id(), messagesstate.messages, toolstools.schema(), ) tracer.record( model_request, stepstep, okTrue, usageresp.usage, ) msg resp.choices[0].message state.messages.append(msg) if not msg.tool_calls: tracer.record(stop, stepstep, reasonno_tool_call) return state.finalize(msg.content) for call in msg.tool_calls: decision state.permissions.check(call) # 这层是你自己的 if decision.need_approval: state.approvals.park(call, decision) # 挂起等人工 return state.checkpoint(waiting_approval) result tools.execute(call) # 在本地执行 state.messages.append(observation(call, result)) tracer.record(stop, stepstate.max_steps, reasonmax_steps) return state.checkpoint(max_steps)注意permissions.check和approvals.park都留在了你自己的代码里通道层不参与判断。这是刻意的审批策略属于业务通道只负责把请求发出去、把响应拿回来。4.2 Checkpoint 恢复后还能接着跑挂起等审批是最容易出问题的地方。park的时候要把 messages、当前 step、待审批的调用一起序列化进 Checkpoint审批通过之后从同一个 step 继续而不是从头重放整个 Turn。重放的安全边界取决于工具性质只读工具重放几次没什么代价写操作必须带幂等键否则审批一次、执行两次。这个判断同样由 Harness 自己做通道层帮不上忙。4.3 Trace 里该盯的四个字段连跑三轮之后打开 Trace逐个 step 看四件事model_request是否存在、ok是否为 true、单次延迟是否落在合理区间、usage里的 token 数是否异常膨胀。如果某一步只有tool_call没有model_request说明那一步根本没走到通道问题在 Loop 或工具执行里如果 token 数从第二轮开始暴涨那是 Observation 清洗没做好不是通道的问题。验证的判定标准很直接每一步都有成功的model_request长任务在 Checkpoint 恢复后还能继续跑下去通道层就算通了。5. 模型通道配通之后剩下七层别指望通道替你干5.1 Permission Check 与 Approval Store 仍在自己手里通道解决的是「请求发得出去、响应拿得回来」它不解决「这个动作该不该执行」。只读工具的调用可以自动放行涉及 kill session、重启实例、改参数这类动作必须挂起等人工审批。审批单存哪、审批人是谁、超时多久作废全是你自己 Harness 里的逻辑。把这两个概念混在一起后果是通道一换权限策略就跟着丢。分层的目的之一就是让换通道这件事对权限层完全透明。5.2 Observation 清洗与 Stop Policy 的判定点Observation 清洗要在本地做工具返回几万行采样直接塞进 messages上下文很快就被撑满第三轮开始模型开始「忘记」任务目标。裁剪策略、字段白名单、超长截断标记都是清洗层的事。Stop Policy 同理。max_steps到了怎么办、连续两次工具调用返回相同结果怎么办、模型反复要求同一个工具怎么办这些判定点写在 Loop Controller 里和请求发到哪个域名没有关系。5.3 SRE RCA Agent 的落法只读诊断在本地执行危险动作等审批回到那个连接数飙高的案例。让它生成一段查活跃会话的 SQL、解释执行计划、对照两个时间段的采样差异这些都没问题。真正执行要由你在本地客户端或者 SQL*Plus 里跑把输出和报错贴回对话模型接着分析。Codex、Claude Code 这类工具能生成、解释、对照 SQL但它们不会、也不该直接连上生产库或生产机器去执行。危险动作在 Loop 里走 Approval Store只读诊断由人在本地跑——这条边界守住了Harness 才谈得上生产级。6. 这一层的排障401、模型名不认、第二轮开始失败6.1 401Key 没进进程环境最常见的形态是代码里写了os.environ[MODEL_API_KEY]但启动脚本里没 export或者只在当前 shell 里 export 了、换到 IDE 的运行配置就没带上。表现是第一个模型请求直接 401Trace 里只有一条失败记录。排查顺序先确认进程环境里真的有这个变量再确认它的值没有多余空格和换行最后确认这把 Key 是从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 控制台创建、没有被删除或轮换。6.2 模型名不认与路径拼错两种表现要分清模型 ID 写错通常是「模型不存在」这类明确的错误响应Base URL 多写一段/v1则是路径找不到表现出来更像 404。两者代码层面都指向同一个坑把会变的值硬编码了。解决办法不是改代码是改环境变量。配置里那些从模型广场复制的 ID不要凭记忆加后缀、加日期、加版本号。列表里是什么就写什么。6.3 不是通道的锅重试风暴与上下文膨胀如果 401 和 404 都没有但 Loop 越跑越慢先看两处。一是重试SDK 的max_retries和 Loop 自己的重试叠在一起一次失败被放大成十几次请求Trace 里能看到密集的model_request。二是上下文Observation 没有裁剪messages 逐轮变长单次延迟和 token 消耗一起往上走。这两类问题改环境变量没用得回到清洗层和 Loop 里改逻辑。7. 配完模型通道之后下一步把 Key 用起来模型调用层配通、Trace 里每一步model_request都绿了之后建议先做两件小事。用同一把 Key 在 模型对话 里发一条测试消息确认模型 ID 和通道没填错如果你打算长期让 Harness 跑长任务顺手去 Coding Plan 看一眼套餐是否撑得住高频调用。Key 的管理入口在 控制台 API Keys多环境建议建多把方便按环境排查和轮换如果你也在用 Claude Code 改这套 Harness环境变量的完整对照表在 Claude Code 接入文档。从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 拿到的这把 Key最终要落到的位置很具体harness/model_client.py里那两行os.environ以及启动脚本里的三个 export。填对了SRE RCA Agent 那种「只读诊断自动跑、危险动作等审批」的长任务才真的能在 Checkpoint 恢复后继续往下走填错了你会在 Trace 的第二步就看见它停下来。