ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

编码智能体决策层实践:双引擎分工降低延迟与Token消耗

编码智能体决策层实践:双引擎分工降低延迟与Token消耗

我一直在折腾一个叫 Jev 的编码智能体,做的事情简单说就是:让程序自己读仓库、改代码、跑测试、修问题。前两版跑下来,我发现最头疼的还真不是模型不会写代码,而是它太能“纠结”了。每走一步都要反复权衡:先看哪个文件、报错先查哪段、修完跑不跑测试、同一种错误换不换方案。这些单看都不难,但组合在一起,每一步都要消耗大量 token 和等待时间。

后来我换了个思路:与其让一个全能模型在“生成代码”和“做决策”之间来回切换,不如单独加一层快速决策能力,把 Claude Code 和 Codex 两个外部模型当作不同特性的决策引擎接进来——一个负责快速判断下一步动作,一个负责复杂情况的深度规划。接入之后,Jev 的整体决策延迟降了将近 60%,测试失败修复的成功率也明显上来了。这篇文章就完整聊聊这个方案:为什么编码 Agent 需要决策层、两个引擎怎么分工、代码怎么落地、实测数据如何,以及我踩过的一堆坑。

1. 为什么编码智能体需要“决策层”而不是“更强的生成模型”

1.1 编码 Agent 的真实瓶颈:选择比生成更贵

很多人刚接触编码 Agent 时,以为难点全在“让模型写出正确代码”。实际跑起来你才会发现,模型写代码本身通常不差,真正拖垮效率的是它做选择的成本太高。

编码 Agent 的工作循环可以简化成“观察 - 决策 - 执行 - 验证”四步。观察是收集当前状态,决策是确定下一步动作,执行是真正改代码或跑命令,验证是检查结果。整个循环里,决策虽然不产生代码,但它决定了后面每一步是否有意义。如果方向选错了,后面所有生成和验证都是白费。比如一个测试用例挂了,Agent 得先判断:是产品代码的问题,还是测试代码本身写错了?是该修实现,还是该改断言?这个“先判断”的动作,如果让一个通用大模型自由发挥,它可能要先读完整错误栈、翻好几个文件、在内部推理半天,才给出一个方向,期间消耗了几千 token。

我做过一次统计:在 Jev 早期版本里,每个动作的决策环节平均要消耗约 1800 到 2500 token。而真正执行一个单文件修改,只需要 300 到 500 token。也就是说,光“决定做什么”的成本就是“实际去做”的好几倍。延迟上更明显,一次带多轮推理的决策经常要 3 秒以上,用户体感就是“Agent 像个迟钝的实习生,半天憋不出一句话”。

1.2 为什么把 Claude Code 和 Codex 当作外部决策源接入

既然决策是瓶颈,最简单的解法是不是换一个更强的模型?我试过。模型越强,单次决策质量确实越高,但延迟和成本也跟着涨,而且大模型有一个通病:它倾向于把所有问题都当成“复杂问题”来处理,哪怕你只是让它判断“这个文件改完要不要重新编译”,它也会给你来一段结构化分析。这种过度思考在编码 Agent 场景里非常致命。

所以我换了一种设计:不让 Jev 去造一个“超级模型”,而是把现有的模型能力重新编排。Claude Code 和 Codex 这两个接口在代码场景里各有所长——一个对快速判断、短上下文动作选择更灵敏,响应节奏更快;另一个对长上下文理解、多文件规划、复杂修复策略更稳。把它们包装成 Jev 的一个“决策层”,核心是三个职责:第一,把“下一步做什么”这种开放问题压成结构化动作指令;第二,用低延迟把明显不靠谱的路径过滤掉;第三,遇到搞不定的情况,主动升级给深度引擎处理。

说白了,决策层解决的问题不是“能力不足”,而是“能力放错了位置”。你把一个擅长做复杂规划的引擎拿来回答“现在该不该跑测试”,就好比让主任医师去分诊台量体温,不是不行,是大材小用还耽误时间。

2. Jev 的架构设计:两个决策引擎各司其职

2.1 快决策引擎与深决策引擎的分工

我在设计 Jev 的决策层时,给两个外部接口定了完全不同的职责边界。

快决策引擎对应 Codex 这一类接口,负责的是轻量化、高频率的动作决策。比如“当前测试失败,下一步是读错误栈对应的第几行”“这个文件已经修改完,是否执行编译命令”“还剩 10 个 lint 错误,优先处理哪一类”。这类决策的特点是:输入上下文不大、选项有限、需要快速给结果。快决策引擎的输出被强制限制成一段很短的 JSON,包含动作类型、目标参数、置信度。

深决策引擎对应 Claude Code 这一类接口,负责的是重规划。比如“这个重构涉及 6 个文件,正确的改动顺序是什么”“这个错误在模块 A 出现过,但根因可能在模块 B,需要给出排查路径”“一个失败的集成测试,涉及数据库迁移和缓存逻辑,怎么定位”。这类决策需要读多段代码片段,甚至需要跨文件对照,一次消耗几千 token 也正常,但值得。

我用一个生活化的类比来解释这套分工:快决策引擎像急诊分诊护士,看一眼症状就能判断你去哪个科室;深决策引擎像主任医生,拿到检查报告之后才给治疗方案。Jev 本身是护士台,负责决定“这个情况该找谁”。

下面是两个引擎在 Jev 里的参数定位:

维度快决策引擎深决策引擎
输入规模压缩后的状态卡片,通常 800 字以内详细上下文片段,可达数千字
输出格式短 JSON,动作 + 参数 + 置信度多步骤规划文本
目标延迟0.5 到 2 秒3 到 10 秒
典型任务下一步动作选择、单文件操作确认跨文件重构、复杂测试失败修复
成本定位低,可高频调用高,仅在升级时调用

2.2 置信度驱动的升级通道

双引擎结构听着简单,真正难的是怎么决定“什么时候该升级”。我一开始用的规则很粗暴:任务涉及文件数量超过 2 个就交给深决策引擎。结果发现误判率很高,有些涉及 3 个文件的小改动,快决策引擎 1 秒就能搞定,完全没必要升级。后来改成置信度驱动。

核心逻辑是这样的:快决策引擎每次返回动作时,必须带一个confidence字段,取值范围 0 到 1,模型自己对这次判断的把握程度。Jev 拿到后,先把任务类型和置信度放进同一个决策门:

  • 如果confidence >= 0.6且任务不是复杂类型,直接采纳快决策结果;
  • 如果confidence < 0.6,无论任务多简单,都升级给深决策引擎重新判断;
  • 如果任务属于“跨文件规划”“架构调整”“不明错误反复出现”这三类,无论置信度多少,直接升级。

这个 0.6 的阈值是我反复测出来的。把它降到 0.3,会出现快决策盲目自信、错误动作被直接采纳的情况;把它升到 0.9,大多数决策都会被升级,快决策引擎形同虚设,延迟又回去了。0.6 在 Jev 里的表现是:大约 72% 的轻量任务能留在快决策层,同时错误升级率只有 11% 左右。

升级通道还有一个附带好处:它能产生高质量的训练标注数据。每一条被深决策引擎纠正过的案例,我都会把“快决策的错误输出”和“深决策的正确输出”存下来,之后可以用来微调一个更便宜的本地决策模型,这个后面再说。

3. 落地实现:API封装、Prompt模板与路由规则

3.1 统一模型客户端封装

代码层面,我先把两个外部接口封装成完全对称的客户端。也就是说,不管底层调用的是哪个,对外都暴露一个decide(card)方法,接收一张决策卡片,返回一个结构化的Decision对象。这样 Jev 主流程不需要关心上游是哪个模型,只在配置里写清楚快决策和深决策分别指向哪个客户端。

下面是决策层的基础封装结构。实际代码里我省略了鉴权细节,核心是三个部分:独立的超时参数、统一的入参出参结构、错误分类。

# decision_layer.py import os import json import time import httpx class QuickDecisionEngine: """快决策引擎,面向低延迟、短输出场景""" def __init__(self): self.api_key = os.environ["QUICK_DECISION_API_KEY"] self.endpoint = os.environ["QUICK_DECISION_ENDPOINT"] self.client = httpx.Client( base_url=self.endpoint, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=httpx.Timeout(2.5, connect=1.0), ) def decide(self, card: dict) -> dict: resp = self.client.post("/v1/decide", json={"card": card}) resp.raise_for_status() return self._parse_decision(resp.json()) def _parse_decision(self, raw: dict) -> dict: # 统一的决策结构 return { "action": raw.get("action"), "params": raw.get("params", {}), "confidence": float(raw.get("confidence", 0.0)), "reason": raw.get("reason", ""), } class DeepDecisionEngine: """深决策引擎,面向长上下文、多步骤规划场景""" def __init__(self): self.api_key = os.environ["DEEP_DECISION_API_KEY"] self.endpoint = os.environ["DEEP_DECISION_ENDPOINT"] self.client = httpx.Client( base_url=self.endpoint, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=httpx.Timeout(12.0, connect=1.0), ) def decide(self, card: dict) -> dict: resp = self.client.post("/v1/plan", json={"card": card}) resp.raise_for_status() return { "plan": resp.json().get("plan", []), "reason": resp.json().get("reason", ""), }

这里的超时参数需要注意。读超时和连接超时我分开设置了,快决策引擎连接超时 1 秒、读超时 2.5 秒,深决策引擎读超时放到 12 秒。原因是两个引擎的使用场景完全不同:快决策如果 2.5 秒还没返回,说明这个模型本身就响应不过来,与其干等,不如走降级;深决策涉及长上下文处理,模型思考时间本身就要 5 到 10 秒,给太短反而会在正常工作时触发超时误判。

3.2 决策卡片 Prompt 的关键设计

决策层的模型输出质量,很大程度取决于决策卡片的 Prompt 设计。我踩过“把整个仓库塞进上下文”的坑之后,重新设计了卡片结构,核心原则是:只给摘要,不给全文;只给选项,不给自由发挥。

一张标准的决策卡片会包含五块信息:

  • 任务类型:枚举值,如select_next_file、confirm_build、fix_test、refactor_plan
  • 当前状态摘要:当前正在处理哪个文件、执行了什么命令、得到了什么返回
  • 文件地图:仓库里和当前任务相关的文件路径及一行描述,不包含文件内容
  • 可用工具列表:Jev 在下一步可能调用的工具名加参数约束,严格枚举
  • 输出约束:只输出 JSON,字段名固定,禁止额外解释

我给快决策引擎设计的 Prompt 模板大概是这样的:

你是 Jev 编码智能体的决策模块。你的任务是从给定选项中选择下一步动作。 任务类型:{task_type} 当前状态摘要:{state_summary} 相关文件: {file_map} 可选动作(只能输出其中之一,严格使用动作名和参数格式): {available_actions} 输出要求: 1. 只输出一个 JSON 对象,不要输出任何解释性文字。 2. JSON 格式:{"action": "动作名", "params": {...}, "confidence": 0.0-1.0, "reason": "不超过10个字"} 3. confidence 低于 0.5 时,必须如实降低数值,不要强行自信。 4. 如果没有把握,选择 "escalate" 动作。

这个模板看着简单,实际上每句话都有用意。第 3 条尤其重要,模型在不确定时往往倾向于给出高置信度,如果不做约束,置信度机制就失效了。第 4 条给了模型一个体面的“投降通道”,它知道自己可以主动请求升级,而不是在几个不靠谱选项里硬选一个。

3.3 路由逻辑、缓存与熔断降级

决策层的路由逻辑是整个接入的中枢。我实现了一个JevRouter,它负责协调快决策、深决策、缓存和熔断状态。核心流程如下:

# router.py class JevRouter: def __init__(self, quick_engine, deep_engine, cache=None): self.quick = quick_engine self.deep = deep_engine self.cache = cache or {} self.failure_count = 0 self.breaker_open = False self.breaker_open_until = 0 async def decide(self, card: dict) -> dict: cache_key = self._build_cache_key(card) if cache_key in self.cache: return self.cache[cache_key] # 熔断状态检查 if self.breaker_open and time.time() < self.breaker_open_until: return await self._local_fallback(card) try: quick_result = await self.quick.decide(card) self.failure_count = 0 except Exception as exc: self._record_failure() return await self._escalate_with_excuse(card, exc) # 是否升级 need_escalate = ( quick_result.get("action") == "escalate" or quick_result.get("confidence", 0) < 0.6 or card.get("task_type") in {"refactor_plan", "architecture", "mystery_bug"} ) if need_escalate: deep_result = await self.deep.decide(card) decision = deep_result decision["source"] = "deep" else: decision = quick_result decision["source"] = "quick" # 校验动作合法性 if not self._validate_action(decision): decision = await self.deep.decide(card) decision["source"] = "deep_after_validation" self.cache[cache_key] = decision return decision

这里有几个细节值得展开。第一个是动作合法性校验。快决策引擎是模型,模型就有幻觉,可能输出一个不在工具列表里的动作名。如果直接执行,轻则报错,重则把工程环境搞乱。所以我会在路由里加一道校验,检查action是否在available_actions枚举里,检查params是否满足该动作的 schema。不合法就强制升级深决策,而不是直接执行错误动作。

第二个是熔断器。外部接口不可能永远稳定。如果快决策引擎连续失败 5 次,我会打开熔断,让它休息 10 秒,期间所有决策都走本地规则引擎。本地规则引擎是我的兜底方案,它不做复杂推理,只是按硬编码规则执行简单判断,比如“测试失败优先查看错误栈最后 50 行”“文件不存在时搜索仓库根目录”。功能很简陋,但能保证 Jev 就算外部接口全挂,也不会彻底卡死。

缓存是最后一个重要组件。我以(仓库文件hash, 任务类型, 当前错误摘要的hash)作为缓存键,TTL 设为 60 秒。效果很明显,修复循环里常常会出现同一个错误被反复检查三次的情况,有缓存之后,第二次和第三次的同类决策直接命中,连外部调用都省了。

4. 接入后实测:延迟、成本与成功率

4.1 三组关键数据对比

整个改造完成之后,我在模拟项目 X 上跑了大概一周的回归测试。这个模拟项目 X 是一个人维护的中型代码库,约 40 个模块、300 多个源文件,测试用例覆盖主体业务逻辑。对比维度取了四个:单步决策延迟、跨文件重构规划时间、单会话 token 消耗、测试失败修复的首次成功率。

指标接入前接入后
单步决策平均耗时3.2 秒0.8 秒
跨文件重构规划耗时8.5 秒3.1 秒
单会话决策 token 消耗基准降低约 55%
测试失败首次修复成功率62%84%

接入前的设计是所有决策都走同一个主模型,每次决策都会输出一段分析再加一个结论,token 消耗很高。接入后,快决策引擎的回应通常在 100 token 以内,相当于把原来“大量内部推理 + 少量决策结论”的结构改成了“直接输出决策结论”,Token 成本自然下降。这里需要注意,这个下降只统计了决策环节的 token,不包含实际生成和修改代码的部分。

4.2 分场景效果拆解

为了更直观地说明变化,我拆了四个典型场景来看。

场景一是单文件小修改,比如改一个函数名、补一个条件判断。这类任务接入前也要走完整决策链,平均 3 秒。接入后基本都由快决策引擎直接搞定,全过程 0.8 秒左右,慢的时候也就 1 秒多,体感接近“秒回”。

场景二是跨文件重构。比如把某个公共工具函数从一个模块挪到另一个模块,同时更新所有调用方。这类任务涉及文件多,快决策引擎会主动输出低置信度,或者直接给escalate,路由就会把它升级到深决策引擎。深决策引擎会先产出一个改动顺序规划,再让 Jev 按规划执行。整体规划时间从原来的 8.5 秒降到 3.1 秒,主要是因为快决策引擎先做了一个“哪些文件与本次重构相关”的过滤,省去了深决策引擎从全仓库找线索的时间。

场景三是测试失败修复,这是收益最明显的一块。之前 Jev 拿到失败测试后,倾向于先读整个错误日志,把一大堆信息塞进上下文,再开始推理。接入后,快决策引擎先做一个“定位动作”,指定去读错误栈最后 50 行对应的源文件。这个动作看起来微不足道,但它极大地缩小了深决策引擎的上下文范围,再配合缓存机制,修复同一个错误时不会反复读取相同的内容。首次修复成功率从 62% 提到 84%,我认为主要就是靠这个。

场景四是误判纠错。有一次快决策引擎对一个“测试环境配置错误”给出了低置信度的错误动作,判断为需要修改某个工具类的实现。路由检测到置信度只有 0.35,升级给深决策引擎,深决策引擎从异常堆栈里发现是配置文件里的连接串格式问题,方向完全不同。这种案例不算多,但足够说明置信度机制的价值——它不是为了让系统更快,而是为了让系统在快的同时还有机会纠错。

4.3 不能忽视的负面代价

有收益就有代价。接入外部决策引擎之后,Jev 多了两层网络依赖,整套系统的可用性上限取决于最不稳定的那个依赖。快决策引擎偶尔会抽风返回空结果,深决策引擎在请求高峰期延迟能从 5 秒涨到 15 秒。为了应对,我不得不把熔断器和本地规则引擎做得更健壮,这本身就是额外工作量。

另一个代价是调试复杂度上升。以前一个决策问题,我只要看主模型的输出日志就行。现在要同时看快决策输出、置信度、是否升级、深决策的规划、缓存是否命中,整个链路长了一倍。我后来做了一个决策日志面板,把每一次决策的输入、输出、来源、耗时、校验结果全部记录下来,遇到问题直接回放,才把这个调试成本的坑填上。

5. 常见问题与排查实录

5.1 API 限流与响应抖动

接入外部接口之后,最先遇到的就是限流。快决策引擎定位是高频调用,结果在实际运行时,修复一个测试可能连续触发 20 多次决策请求,很快就打到限流阈值,返回 429。

我的处理分了三层:第一层是请求端限速,在 JevRouter 里加了一个简单的令牌桶,每秒最多放行 15 个决策请求;第二层是重试策略,对 429 响应做指数退避,退避时间从 1 秒开始,每次翻倍,最多退到 8 秒;第三层是熔断保护,如果 30 秒内错误率超过 20%,直接熔断快决策引擎,切换到深决策或本地规则。这三层做完之后,决策层的可用性从 95% 提到了 99.2%。

响应抖动是另一个难题。深决策引擎偶尔会出现连接正常但读取超时的情况,表现为 Jev 卡了十秒然后报错。排查后发现是上游打印了超大长度的响应体,解析 JSON 时拖慢了响应时间。我的对策是把响应的max_tokens上限明确控制住,并在客户端层面对响应体大小做限制,超过 2MB 直接丢弃并按超时处理。

5.2 模型输出 JSON 不稳定

快决策引擎虽然约束了输出格式,但它毕竟是个生成模型,偶尔还是会返回带 Markdown 代码块的 JSON、在 JSON 前后贴解释文字、甚至夹带注释符号。我一开始直接用json.loads解析,结果每五六个请求就挂一次,非常影响体验。

后来我写了一个健壮的决策解析器,分三步走:

# robust_decision_parser.py import json import re def parse_decision_response(raw_text: str) -> dict: # 1. 优先提取代码块中的内容 fence_match = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", raw_text, re.S) if fence_match: raw_text = fence_match.group(1) # 2. 找到第一个 { 和最后一个 },截取候选区间 start = raw_text.find("{") end = raw_text.rfind("}") if start == -1 or end == -1 or end <= start: raise ValueError("no json object found") candidate = raw_text[start:end + 1] # 3. 尝试标准解析,失败则清洗 try: return json.loads(candidate) except json.JSONDecodeError: # 清洗常见问题:尾逗号、单引号、注释符号 candidate = re.sub(r",\s*}", "}", candidate) candidate = re.sub(r"'", '"', candidate) candidate = re.sub(r"//.*", "", candidate) return json.loads(candidate)

这个解析器上线之后,JSON 解析失败率从 2% 降到了接近 0。但我还是留了一手:如果清洗之后仍然解析失败,就把这次请求判为失败,走重试或升级通道,绝不允许异常输出直接进入决策流程。

5.3 决策引擎“乱选工具”与幻觉问题

最危险的问题不是格式,而是语义错误。快决策引擎有几次给出了完全合理的 JSON,但动作指向完全错误。比如当前正在处理“A 模块编译失败”,它建议去修改“B 模块的配置文件”,理由完全站不住脚。这就是典型的模型幻觉。

我最初的方案是完全依赖置信度字段,后来发现不够。模型在幻觉状态下照样敢给 0.9 的置信度,所以我在路由层加了三道硬约束:第一,动作名必须存在于工具枚举列表,这个前面提到过;第二,动作参数里的文件路径必须经过文件系统校验,文件不存在就直接判为非法;第三,动作涉及的模块必须与当前任务类型有关联,比如fix_test类型的决策,无法把最终动作跳到与测试文件无依赖关系的模块上。这些校验都通过之后,才会真正执行动作;校验失败时强制升级深决策引擎。

还有一个经验是:不要让外部决策引擎直接接触执行权限。Jev 里的决策引擎只负责输出“建议动作”,真正执行动作的是本地校验之后的工具调度层。也就是说,决策引擎永远处在一个“提建议”的角色,而不是“发布命令”的角色。这条边界线既然划出来,很多安全问题天然就被隔离掉了。

最后记录一次翻车经历。某次深决策引擎给出一个 5 步重构计划,其中第 3 步是把缓存模块的清理逻辑放到另一个模块里。计划本身看起来合理,但实际执行时发现缓存模块依赖那个模块,形成了循环依赖。我后来规定:涉及移动或删除代码的计划,必须额外让深决策引擎输出“受影响依赖列表”,并且由本地构建工具跑一遍依赖分析验证。自那以后,循环依赖这类低级错误再也没有出现过。

最后说点我的体会

这套决策层改造做完,我最深的一个感受是:给 Coding Agent 加能力的思路,不一定非得“换更大更强的模型”,也可以从“让合适的模型干合适的活”入手。快决策引擎的好处不是它比原来的主模型聪明,而是它响应快、输出短、成本低,把高频小决策从复杂模型手里解放出来;深决策引擎负责兜底复杂场景,保证整体决策质量不滑坡。两者中间靠置信度机制衔接,再用熔断、缓存、校验这些工程手段兜住可靠性。如果你也在搭编码智能体,我建议不要一上来就照搬全套架构。先把决策日志打全,看看你的 Agent 到底在哪些环节上“想太久”“想太贵”,然后只挑最高频的那类决策接入快决策引擎,跑通闭环之后再慢慢扩展升级通道。把第一步做小了,后面反而走得更快。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进