feat(v2): T-M3 采纳 llmrouter pinch 三档裁剪——Architect 渲染超预算按相关性分档保留
- Workspace.render_for_architect:超 1200 token 时对 archive 行按与 query+goal 的字符 2-gram Dice 相似度分档——sim<0.25 先丢、sim<0.55 截断为 40% 摘要、 仍超限从低到高继续丢;保留行维持原插入序(前缀稳定),最近 4 行下限不变 (相比旧'从最旧整段丢',高相关事实在预算内留存更久) - 顺带消除两项旧债:压缩态与首次渲染共用同一 parts 结构(格式漂移)、 裁剪循环不再每轮 deepcopy 全文档 - 修复本轮引入的缺陷:会话轮次 tool_calls 为 int 计数时清单提取迭代崩溃 (test_session_multi_turn 抓出,已加类型防御) - 新增 tests/test_pinch_trim.py 4 项;全量 244 passed ×2(基线 230)
This commit is contained in:
+76
-29
@@ -450,15 +450,17 @@ class Workspace:
|
||||
for p in d.get("progress", [])[-5:]:
|
||||
parts.append(f"{p.get('step')} [{p.get('status')}] {_clip(p.get('summary', ''), 40)}")
|
||||
parts.append("== archive ==")
|
||||
for line in d.get("archive", [])[-8:]:
|
||||
parts.append(line)
|
||||
# token 预算:超限先截断最旧 archive(已只保留 3 条 decisions)
|
||||
archive = d.get("archive", []) or []
|
||||
shown = list(archive[-8:])
|
||||
parts.extend(shown)
|
||||
# token 预算:超限按 pinch 三档裁剪 archive(T-M3,采纳 llmrouter pinch
|
||||
# keep/summarize/drop 分档思想),决策见 _trim_archive_lines
|
||||
out = "\n".join(parts)
|
||||
while estimate_tokens(out) > 1200 and len(d.get("archive", [])) > 4:
|
||||
d = copy.deepcopy(d)
|
||||
d["archive"] = d["archive"][4:]
|
||||
out = "\n".join(_rerender(self, d))
|
||||
return out
|
||||
if estimate_tokens(out) <= 1200 or len(shown) <= 4:
|
||||
return out # 预算内 / 已在下限(与旧实现的 4 行下限一致)
|
||||
head = parts[:-len(shown)]
|
||||
ref_text = d.get("query", "") + " " + (d.get("brief") or {}).get("goal", "")
|
||||
return _trim_archive_lines(head, archive, shown, ref_text)
|
||||
|
||||
def render_for_worker(self, step_id: str,
|
||||
artifact_text: Optional[str] = None) -> str:
|
||||
@@ -525,24 +527,69 @@ class Workspace:
|
||||
return self.data
|
||||
|
||||
|
||||
def _rerender(ws: "Workspace", d: Dict[str, Any]) -> List[str]:
|
||||
"""用裁剪后的数据重建 Architect 渲染(供超限压缩内部用)。"""
|
||||
parts: List[str] = []
|
||||
m = d["meta"]
|
||||
parts.append("== meta ==")
|
||||
parts.append(f"status={m['status']} round={m['round']}")
|
||||
parts.append("== query ==")
|
||||
parts.append(_clip(d["query"], LIMITS["query_truncate"]))
|
||||
parts.append("== issues ==")
|
||||
for iss in d.get("issues", []):
|
||||
parts.append(f"{iss['id']} step={iss.get('step')} ask={_clip(iss.get('ask', ''), 80)}")
|
||||
parts.append("== decisions(最近3) ==")
|
||||
for dec in d.get("decisions", [])[-3:]:
|
||||
parts.append(f"ref={dec.get('ref')} reply={_clip(dec.get('reply', ''), 80)}")
|
||||
parts.append("== progress ==")
|
||||
for p in d.get("progress", [])[-5:]:
|
||||
parts.append(f"{p.get('step')} [{p.get('status')}] {_clip(p.get('summary', ''), 40)}")
|
||||
parts.append("== archive ==")
|
||||
for line in d.get("archive", [])[-6:]:
|
||||
parts.append(line)
|
||||
return parts
|
||||
def _bigrams(text: str) -> Dict[str, int]:
|
||||
"""字符 2-gram 计数(去空白、小写;零依赖轻量相关性度量)。"""
|
||||
t = "".join(text.lower().split())
|
||||
out: Dict[str, int] = {}
|
||||
for i in range(len(t) - 1):
|
||||
g = t[i:i + 2]
|
||||
out[g] = out.get(g, 0) + 1
|
||||
return out
|
||||
|
||||
|
||||
def _dice(a: Dict[str, int], b: Dict[str, int]) -> float:
|
||||
"""Dice 系数:2 * 交集 / (|a| + |b|),空集返回 0。"""
|
||||
if not a or not b:
|
||||
return 0.0
|
||||
inter = sum(min(v, b.get(k, 0)) for k, v in a.items())
|
||||
return 2.0 * inter / (sum(a.values()) + sum(b.values()))
|
||||
|
||||
|
||||
def _trim_archive_lines(head: List[str], archive: List[str],
|
||||
shown: List[str], ref_text: str) -> str:
|
||||
"""Architect 渲染超预算时的 archive 三档裁剪(T-M3,采纳 llmrouter pinch
|
||||
keep/summarize/drop 分档思想)。
|
||||
|
||||
相关度 = 行与 query+goal 的字符 2-gram Dice 系数:
|
||||
第一档:丢 sim<0.25 的行(最低相关先丢);
|
||||
第二档:把 sim<0.55 的行截断为 40% 摘要;
|
||||
第三档:仍超限则从低相关到高相关继续丢。
|
||||
保留行维持原插入序(前缀稳定);任何情况下至少保留 4 行,
|
||||
达到下限仍超限则接受溢出(与旧实现一致)。
|
||||
相比旧"从最旧起整段丢弃",高相关事实在预算内留存得更久;
|
||||
且压缩态与首次渲染共用同一 parts 结构(消除格式漂移与每轮 deepcopy)。
|
||||
"""
|
||||
ref = _bigrams(ref_text)
|
||||
start = len(archive) - len(shown)
|
||||
idx = list(range(start, len(archive)))
|
||||
sims = {i: _dice(_bigrams(archive[i]), ref) for i in idx}
|
||||
order = sorted(idx, key=lambda i: (sims[i], i))
|
||||
|
||||
kept = set(idx)
|
||||
clips: Dict[int, str] = {}
|
||||
|
||||
def _over_budget() -> bool:
|
||||
lines = [clips.get(i, archive[i]) for i in sorted(kept)]
|
||||
return estimate_tokens("\n".join(head + lines)) > 1200
|
||||
|
||||
# 第一档:丢低相关
|
||||
for i in order:
|
||||
if not _over_budget() or len(kept) <= 4:
|
||||
break
|
||||
if sims[i] < 0.25:
|
||||
kept.discard(i)
|
||||
# 第二档:中相关截断为摘要(40% 长度)
|
||||
for i in order:
|
||||
if not _over_budget():
|
||||
break
|
||||
if i in kept and sims[i] < 0.55:
|
||||
clips[i] = _clip(archive[i], max(20, int(len(archive[i]) * 0.4)))
|
||||
# 第三档:仍超限从低到高继续丢
|
||||
for i in order:
|
||||
if not _over_budget() or len(kept) <= 4:
|
||||
break
|
||||
kept.discard(i)
|
||||
clips.pop(i, None)
|
||||
|
||||
lines = [clips.get(i, archive[i]) for i in sorted(kept)]
|
||||
return "\n".join(head + lines)
|
||||
|
||||
Reference in New Issue
Block a user