搞了一整夜把一套自建的智能体运行时圈子里常说的 Harness从零跑通顺手拿了某国产推理模型和某海外终端编码 Agent 做对照。结论先摆出来差距没有宣传得那么玄乎但方向性差异非常明显——一个赢在长上下文承载力和成本一个赢在工具调用的纪律性。这篇东西把实验设计、核心代码、踩坑记录全摊开你照着能自己复现一遍。下文为了不惹麻烦国产那套简称 A海外那套终端壳简称 B名字不重要重要的是它们在架构取舍上的分歧。适合已经能调通对话接口、想往让模型真的动手改代码迈一步的人看也适合被各种 Agent 宣传稿绕晕、想找个能落地的参照系的人。1. 先把 Harness 这个词说清楚它到底在管什么1.1 从会聊天到能干活中间差的是运行时很多人对 Harness 的理解停留在套一层 prompt这是最大的误解。模型本身只会输出 token它不知道磁盘上有什么文件、不知道上一条命令的退出码、更不知道自己上一轮改的那行代码有没有让测试变红。Harness 干的事情本质上是给模型造一个感官系统加一副手脚把文件系统、命令行、测试结果翻译成模型能读的文本再把模型输出的结构化意图翻译成真实的系统调用。这个翻译层做得好不好直接决定了同一个模型的表现能差出三成以上。我自己的观察是绝大多数某个模型写代码不行的抱怨追根溯源都是 Harness 的锅。工具描述写得含糊模型就会拿着read_file去读目录错误信息只回一个Error模型就只能在原地瞎猜上下文不做裁剪跑到第 20 轮的时候模型已经忘了任务是什么。这些都不是模型能力问题是壳没搭好。还有一个容易被忽略的点Harness 决定了模型的行动节奏。同一个模型如果你每次只让它做一件事、做完立刻把结果喂回去它会显得很谨慎、很稳如果你允许它一次性吐五个工具调用它就会变得激进成功率上升但偶尔会翻车。这个节奏是可以调的而且调它比换模型便宜得多。1.2 为什么我不直接调 API而是自己搭壳有人会问现成的终端 Agent 用着挺顺为什么要自己造。三个理由。第一是可控性现成工具的行为对你是不透明的它什么时候压缩上下文、什么时候重试、什么时候偷偷跳过一步你根本看不见出了问题只能猜。自己搭的壳每一轮的消息数组你都能打印出来看这是排障的基础。第二是可替换性壳和模型解耦之后换模型就是改一个字符串你可以拿同一套任务集去横评而不是被某个厂商绑死。第三是领域适配通用 Agent 面向的是随便什么代码库而你的项目可能有特殊的构建流程、特殊的目录约定、特殊的测试入口这些只有自己写工具才能塞进去。当然代价也很实在你得自己处理流式响应的增量拼接、自己设计工具 schema、自己做沙箱、自己处理超时和重试。这一套下来一晚上是打不住的我这次是拿之前攒的底子改的从零写估计得两三个晚上。所以我的建议是先用现成工具跑两周摸清楚自己到底缺什么再决定要不要自己搭。1.3 两套壳的定位差异通用 Agent 壳 vs 终端编码壳把 A 和 B 放在一起比其实比的不是模型是两套壳的设计哲学。B 那套是为终端编码场景深度收敛过的工具集很小基本就是读文件、写文件、跑命令、搜索这四个每个工具的描述都写得极长极细甚至把什么时候不该用我都写进去了。这种设计的思路是牺牲灵活性换稳定性让模型在有限的动作空间里做选择出错概率自然低。A 这边我拿到的是更通用的接口形态工具集可以自己随便加schema 自由度很高但反过来模型对工具的理解完全依赖你写的描述。我一开始图省事工具描述就写了一行结果模型经常拿write_file去干apply_patch的活整文件重写一遍把人家精心排版的代码格式全冲了。后来把描述补到六七百字加了反例和边界条件误用率肉眼可见地往下掉。这个差异在实测里体现得很明显B 的首轮工具选择准确率是 94%A 在我把描述补全之前只有 68%补全之后爬到 91%。也就是说A 不是不会用工具是需要你把说明书写明白。这对愿意投入精力调 prompt 的人是好事对想开箱即用的人是负担。2. 实测方案设计我怎么把两套东西拉到同一条起跑线2.1 基准任务集怎么选横评最容易翻车的地方就是任务集不公平。我见过太多对比评测给 A 的任务是自己精心调过的给 B 的任务是随手抓的结论当然没法看。我这次的做法是先从自己维护的三个小仓库里各抽五个真实的历史提交把提交前的快照做成初始状态把提交信息改写成任务描述然后跑完对比 diff 和测试结果。这样任务天然带验收标准——原来的提交通过了测试你的 Agent 也得通过。具体任务分四类单文件小改改个校验逻辑、补个边界判断、跨文件重构把散落的配置读取收敛到一个模块、测试补齐给没有覆盖的函数写单测、依赖升级后的适配接口签名变了改调用点。前两类占比最高因为它们最常见也最能暴露上下文管理的问题。后两类是压力测试尤其依赖升级那类模型必须先把整个调用链摸清楚才能动手很容易在探索阶段就把上下文吃满。一共 15 个任务每个任务跑 3 次取中位数避免单次运气成分。样本量确实不大所以下面的数字你当趋势看别当结论。2.2 度量口径不看感觉聪明看四个硬指标我盯的是四个数。任务完成率测试全绿且 diff 落在合理范围内才算通过只把测试改绿但把断言删了的一律判失败这个坑必须堵死。平均工具调用轮次完成任务消耗了多少次模型输出工具调用→执行→回灌结果的循环这个数字直接反映效率轮次越多成本越高、越容易跑偏。首轮工具选择准确率任务给进去之后模型第一个调用的工具是不是合理的这个指标最能反映工具描述的质量。单任务成本按各自的计费口径折算成相对值。另外我单独记了一项长上下文任务成功率故意选了几个需要读十几个文件才能下手的任务看模型在上下文堆到两三万 token 之后还能不能保持清醒。这一项是我最关心的因为它决定了这套东西能不能用在真实的中大型项目上。2.3 沙箱与回放让结果可复现沙箱这块我踩过血的教训。第一版没做隔离模型一句rm -rf差点把我工作目录扬了——它是想清理临时文件路径算错了。从那以后我加了三道锁所有文件路径必须 resolve 之后落在 workspace 根目录内命令行只允许白名单里的可执行文件任何写操作先备份到.bak目录。回放机制是另一个必需品。每一轮我都把完整的 messages 数组、工具调用参数、执行结果、耗时写进一个 jsonl 文件一个任务一个文件。好处是出了问题不用重跑——直接把 jsonl 拉出来看第几轮开始跑偏、模型当时看到了什么、工具返回了什么一目了然。我这次调试 A 的一个反复读同一个文件的毛病就是靠回放发现工具返回里带了时间戳每轮都不一样模型以为文件变了于是一直重读。这种问题你靠盯屏幕是绝对看不出来的。3. 核心实现拆解一个能干活的最小 Harness 长什么样3.1 工具注册表schema 设计决定模型会不会用错工具工具注册这块我用的是装饰器加全局字典的土办法够用。关键不在注册机制在描述文本的写法。我的经验是一个好的工具描述必须包含四段做什么、参数含义含单位和边界、什么时候用、什么时候别用。第四段最容易被省略也最值钱。举个具体的。read_file这个工具我第一版描述是读取文件内容。结果模型经常拿它去读一个两千行的文件一次性把上下文塞爆。改成下面这样之后行为立刻规矩了from pathlib import Path import json TOOLS {} ROOT Path(/workspace/repo).resolve() def tool(name, description, parameters): def deco(fn): TOOLS[name] { type: function, function: {name: name, description: description, parameters: parameters}, fn: fn, } return fn return deco tool( nameread_file, description( 读取工作区内某个文件的文本内容返回带行号的格式。\n 适用需要查看文件具体实现时。\n 不适用只想确认文件是否存在用 list_dir只想找符号位置用 search_code。\n 注意单次最多返回 800 行。文件更长时必须用 start_line/end_line 分段读 否则内容会被截断且末尾标注 [TRUNCATED]不要假设截断后的内容是全文。 ), parameters{ type: object, properties: { path: {type: string, description: 相对于仓库根目录的路径不要用绝对路径不要用 ..}, start_line: {type: integer, description: 起始行号从 1 开始省略则从第一行开始}, end_line: {type: integer, description: 结束行号包含该行省略则读到文件末尾或 800 行上限}, }, required: [path], additionalProperties: False, }, ) def read_file(path, start_lineNone, end_lineNone): p (ROOT / path).resolve() if not str(p).startswith(str(ROOT)): return ERROR: 路径越界只允许访问工作区内文件 if not p.is_file(): return fERROR: 文件不存在 {path} lines p.read_text(encodingutf-8, errorsreplace).splitlines() s (start_line or 1) - 1 e end_line or len(lines) chunk lines[s:e] truncated len(chunk) 800 chunk chunk[:800] body \n.join(f{s i 1:5} | {ln} for i, ln in enumerate(chunk)) if truncated: body \n[TRUNCATED] 后面还有内容请用 start_line 继续读 return body or (空文件)注意几个细节。返回里带行号模型引用行号做后续编辑会准很多越界检查放在最前面别等读了才发现截断要显式标注否则模型会把截断当成文件结束然后自信地告诉你这个文件只有 800 行。additionalProperties: False也建议加上能挡掉一批模型自作主张加参数的调用。3.2 主循环流式解析 工具调用分发主循环是整个壳的心脏。流式响应下工具调用的参数是分片到达的必须按index累积拼接拼完再json.loads。这一步我第一版写错了没按 index 分组多工具并行调用的时候参数串到了一起报了一堆莫名其妙的 JSON 解析错误查了两个小时。import json, time from openai import OpenAI client OpenAI(api_keyYOUR_KEY, base_urlhttps://api.example.com/v1) MODEL your-model-name def run(task, max_turns40, token_budget180_000): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: task}, ] trace [] for turn in range(max_turns): stream client.chat.completions.create( modelMODEL, messagesmessages, tools[{type: function, function: t[function]} for t in TOOLS.values()], temperature0.2, streamTrue, ) content_buf, calls [], {} for chunk in stream: if not chunk.choices: continue delta chunk.choices[0].delta if delta.content: content_buf.append(delta.content) for tc in (delta.tool_calls or []): slot calls.setdefault(tc.index, {id: , name: , args: }) if tc.id: slot[id] tc.id if tc.function and tc.function.name: slot[name] tc.function.name if tc.function and tc.function.arguments: slot[args] tc.function.arguments assistant {role: assistant, content: .join(content_buf) or None} if calls: assistant[tool_calls] [{ id: calls[i][id], type: function, function: {name: calls[i][name], arguments: calls[i][args]}, } for i in sorted(calls)] messages.append(assistant) if not calls: return {ok: True, answer: assistant[content], turns: turn 1, trace: trace} for i in sorted(calls): name, raw calls[i][name], calls[i][args] try: args json.loads(raw) if raw.strip() else {} except json.JSONDecodeError as e: result fERROR: 参数不是合法 JSON{e}。请重新输出完整参数不要省略引号。 else: fn TOOLS.get(name, {}).get(fn) if fn is None: result fERROR: 不存在工具 {name}可用工具{list(TOOLS)} else: t0 time.time() try: result str(fn(**args)) except TypeError as e: result fERROR: 参数不匹配{e}请检查必填项 except Exception as e: result fERROR: 工具执行异常 {type(e).__name__}: {e} trace.append({turn: turn, tool: name, args: args, ms: int((time.time() - t0) * 1000)}) messages.append({role: tool, tool_call_id: calls[i][id], content: result[:20000]}) messages maybe_compress(messages, token_budget) return {ok: False, answer: 达到最大轮次仍未完成, turns: max_turns, trace: trace}这里有几个我觉得必须有的设计。工具结果长度截断result[:20000]看着粗暴但能救命有些命令的输出能到几兆全塞进去直接爆上下文。异常分类返回JSON 解析失败、参数不匹配、工具内部异常三种错误给三种不同的提示语模型看到参数不是合法 JSON会去修格式看到不存在工具会换工具看到执行异常会想别的办法你统一返回Error它就只会重试。达到最大轮次不报异常返回一个结构化的失败结果外层好统计。3.3 文件编辑为什么整文件重写是个坑我一开始给的工具是write_file整文件覆盖。问题很快就来了模型为了改三行代码把整个文件重写一遍然后你会看到一百多行的 diff其中八十行是它顺手优化的——改了缩进、重排了 import、删了它认为没用的注释。评审的时候这种 diff 根本没法看。后来我把主力编辑工具换成apply_patch用唯一匹配的原文片段 → 新片段的方式强制模型只动它真正想动的地方tool( nameapply_patch, description( 用精确的文本替换修改文件。old_text 必须在文件中唯一出现否则拒绝执行。\n 适用修改已有文件的局部内容。\n 不适用新建文件用 write_file。\n 注意old_text 要带足够上下文建议前后各留 2 行以保证唯一 但不要整段复制缩进和空格必须与原文完全一致包括行尾空白。 ), parameters{ type: object, properties: { path: {type: string, description: 相对仓库根目录的路径}, old_text: {type: string, description: 要被替换的原文必须唯一匹配}, new_text: {type: string, description: 替换后的内容删除则传空字符串}, }, required: [path, old_text, new_text], additionalProperties: False, }, ) def apply_patch(path, old_text, new_text): p (ROOT / path).resolve() if not str(p).startswith(str(ROOT)): return ERROR: 路径越界 src p.read_text(encodingutf-8) n src.count(old_text) if n 0: return (ERROR: 未找到匹配。常见原因缩进不一致、行尾空格差异、 或文件已被前一步修改。请重新 read_file 确认当前内容。) if n 1: return fERROR: 匹配到 {n} 处不唯一。请扩大 old_text 的上下文范围。 p.with_suffix(p.suffix .bak).write_text(src, encodingutf-8) p.write_text(src.replace(old_text, new_text, 1), encodingutf-8) return fOK: 已修改 {path}原文件备份为 {p.name}.bak这个改动带来两个连锁好处。一是 diff 干净了评审成本骤降二是模型的思考被迫变得更精确它得先把要改的原文准确抄下来这个过程本身就相当于让它重新读一遍代码改错的概率明显下降。实测下来A 用apply_patch的首次修改正确率比用write_file高了差不多二十个点。匹配失败的错误提示也很关键。我一开始只返回未找到匹配模型就反复用同样的参数重试死循环。加上请重新 read_file 确认当前内容这句之后它基本上第一次失败就会去重读然后带着正确的缩进重来。3.4 上下文预算与压缩策略上下文管理是我认为最能拉开差距的地方。真实项目里模型读十几个文件是常事两三轮下来就上万 token 了。我的策略是分三级处理而不是一上来就粗暴摘要。第一级是工具结果瘦身在工具返回的时候就把冗余信息砍掉。比如跑测试我只返回失败的用例和最后三行汇总通过的用例全部丢掉——模型不需要知道有 200 个测试通过了它只需要知道哪 3 个挂了。比如list_dir我默认只返回两层、跳过.git、node_modules、__pycache__并且限制条目数。这一级能砍掉大概六成的无效 token而且完全不损失信息。第二级是旧的工具结果折叠当总量超过预算的 70% 时触发。保留最近 8 条消息原样不动更早的roletool消息替换成一行摘要比如[已省略 read_file 结果文件 utils/cache.py共 340 行]。注意只折叠工具结果不折叠 assistant 的推理内容和它做过的决定那些是有决策价值的。第三级才是摘要重写实在压不住了超过预算 95%才做把前半段对话整体交给模型总结成一段已完成事项 当前状态 下一步计划然后重建 messages。这一步很贵而且有信息损失我一般尽量避免走到这。def maybe_compress(messages, budget, keep_recent8): total sum(len(str(m.get(content) or )) for m in messages) // 3 # 粗估 token if total budget * 0.7: return messages head, tail messages[:-keep_recent], messages[-keep_recent:] for i, m in enumerate(head): if m.get(role) tool and len(m.get(content) or ) 200: first (m[content] or ).splitlines()[0][:80] head[i] {**m, content: f[已折叠] {first} ...内容过长已省略} return head tail那个/3的粗估是给中文用的英文场景用/4更准。不用精确 tokenizer 是因为压缩决策不需要那么精确误差 20% 完全能接受而引入 tokenizer 会带来额外依赖和一套版本兼容问题。3.5 权限与安全边界这块说到底是防模型好心办坏事。我设了四条线。路径白名单所有文件操作的 resolve 结果必须在 workspace 内这条挡掉了绝大多数误操作。命令白名单只允许python、pytest、node、npm、git且git只允许status、diff、log、add、ls、cat、rg其他一律拒绝。白名单比黑名单可靠得多黑名单你永远想不全。资源限制每个命令超时 120 秒超时直接 kill 并返回部分输出加快照说明。写前备份任何修改先写.bak一个任务结束时统一清理。还有一条我后来加的禁止git commit和git push。理由很简单让模型自己提交你会得到一堆没法看的提交信息而且万一它把调试代码提交上去回滚成本比省下的时间高得多。让它改文件、跑测试提交这一步留给人这个分工我目前没打算改。4. 完整代码实战从零跑通一次真实改造任务4.1 目录结构与依赖整个壳就三个文件别搞得像框架一样复杂越简单越好调。harness/ ├── main.py # 主循环 CLI 入口 ├── tools.py # 所有工具定义 ├── prompt.py # 系统提示词 └── runs/ # 每个任务的 jsonl 回放记录依赖只有openai和一个终端着色库Python 3.10 以上用到了Path.is_relative_to之类的东西。系统提示词我写得比较克制大概四百字核心就三句先看清楚再动手改完必须自己验证不确定就说不确定别编。后面这两句尤其重要能明显减少改完就说完成了的情况。# prompt.py SYSTEM_PROMPT 你是一个在真实代码仓库里工作的工程助手。 工作规则 1. 动手前先搞清楚现状。至少读一遍要改的文件涉及调用链的要把调用方读全。 2. 每次调用工具后先判断上一步的结果是否符合预期再决定下一步。 3. 修改代码必须用 apply_patch提供唯一匹配的上下文。新建文件才用 write_file。 4. 改完必须验证跑相关测试或至少跑一次语法检查。验证不通过不许声称完成。 5. 遇到不确定的地方明确说明这里我不确定不要猜测接口行为。 6. 完成任务后用三句话说明改了什么、为什么这么改、怎么验证的。 可用工具见函数列表。参数格式错误会原样返回错误信息请据此修正。第 6 条是我加的效果意外地好。强制模型做一次自述相当于给它一个自我检查的机会能捞回来一部分改了但改歪了的情况。4.2 工具层代码除了前面写的read_file和apply_patch还需要的工具就三个。list_dir用来摸结构search_code用来定位符号run_command用来验证。写得越少越好工具太多模型反而选择困难。import subprocess, shlex ALLOWED_BINS {python, pytest, node, npm, ls, cat, rg} GIT_SUB {status, diff, log, add} tool( namerun_command, description( 在工作区根目录执行一条命令返回 stdout stderr合并和退出码。\n 适用跑测试、跑 lint、查看 git 状态。\n 不适用查看文件内容用 read_file列目录用 list_dir。\n 注意只允许 pytest/python/node/npm/ls/cat/rg 和 git 的只读子命令 超时 120 秒输出超过 8000 字符会被截断建议配合 -q 或 tail 使用。 ), parameters{ type: object, properties: { command: {type: string, description: 完整命令行例如 pytest tests/test_cache.py -q}, timeout: {type: integer, description: 超时秒数默认 120最大 300}, }, required: [command], additionalProperties: False, }, ) def run_command(command, timeout120): try: parts shlex.split(command) except ValueError as e: return fERROR: 命令无法解析{e} if not parts: return ERROR: 空命令 head parts[0] if head git: if len(parts) 2 or parts[1] not in GIT_SUB: return fERROR: git 只允许 {sorted(GIT_SUB)} elif head not in ALLOWED_BINS: return fERROR: 不允许执行 {head}白名单为 {sorted(ALLOWED_BINS)} try: cp subprocess.run(parts, cwdROOT, capture_outputTrue, textTrue, timeoutmin(timeout, 300)) except subprocess.TimeoutExpired: return fERROR: 命令超时{timeout}s请缩小测试范围或加 -q out (cp.stdout cp.stderr).strip() if len(out) 8000: out out[:4000] \n...[中间省略]...\n out[-4000:] return fexit{cp.returncode}\n{out or (无输出)}search_code我直接用rg包了一层比自己写遍历快得多也省得处理各种编码问题。list_dir的重点是过滤.git、node_modules、dist、__pycache__这些必须跳过不然模型第一次探索就把上下文吃光了。4.3 主循环代码主循环就是前面 3.2 那段加一个 CLI 入口和 trace 落盘。落盘这块我用 jsonl一行一条方便grep和tail。# main.py 片段 import sys, json, time from pathlib import Path def save_trace(task_id, result): Path(runs).mkdir(exist_okTrue) with open(fruns/{task_id}.jsonl, a, encodingutf-8) as f: for item in result[trace]: f.write(json.dumps(item, ensure_asciiFalse) \n) f.write(json.dumps({ok: result[ok], turns: result[turns], ts: int(time.time())}, ensure_asciiFalse) \n) if __name__ __main__: tid, task sys.argv[1], sys.argv[2] res run(task) save_trace(tid, res) print(res[answer]) print(f\n[轮次 {res[turns]} / 工具调用 {len(res[trace])} 次])4.4 跑一个真实任务给项目加缓存层我从历史提交里挑了个典型任务某个数据服务里的配置读取函数每次调用都重新解析一遍文件要加一层带过期时间的进程内缓存。任务描述我改写成这样config/loader.py里的load_config()每次调用都会读文件并解析在高频调用路径上开销明显。请加一层进程内缓存同一路径在 60 秒内重复调用直接返回缓存结果超过 60 秒重新加载。需要注意路径参数可能是相对路径也可能是绝对路径两种都要正确处理。改完跑pytest tests/test_loader.py -q验证。A 的表现过程我按回放记录还原一下。第一轮它调了read_file config/loader.py读到 80 多行然后第二轮调search_code load_config看调用点找到三处。第三轮直接给了apply_patch加了个模块级字典加时间戳。到这儿都对但第四轮跑测试挂了两个因为原测试里有断言缓存不应该跨测试用例生效的用例。它第五轮读了测试文件第六轮改成了按路径 key 分开存时间戳第七轮测试全绿。八轮四分钟出头。中间有个小插曲第三轮的 patch 它把import time加在了文件中间第八轮它自己发现了补了一次 patch 挪到顶部。这个自我修正能力比我预期好但也说明它第一版确实不够严谨。B 那边跑同一个任务用了六轮差异主要在探索阶段——它第一轮就并行发了read_file和search_code两个调用少了一轮往返。而且它一开始就注意到了测试文件里那个缓存隔离的断言提前做了处理。这就是我说的纪律性它更像是把整个任务在脑子里过了两遍才动手。4.5 结果解读这个任务的差异很有代表性。A 是边做边修先出个大概对的版本靠测试反馈迭代B 是先想清楚再做一次成型概率更高。两种路子都能到终点但代价不同A 消耗的轮次多、token 多但每一步你都能看懂B 快但它第一轮那个并行调用如果判断错了方向后面纠偏的成本也高。对我这种习惯盯着看的人来说A 的路子反而更舒服因为每一步都是透明的出问题好定位。但如果是要挂到 CI 里无人值守跑B 的稳定性更让人放心。这个取舍没有标准答案看你的使用场景。5. 实测数据与差异分析强在哪弱在哪5.1 四个指标的横向对比下面这组数字是我那 15 个任务、每个 3 次取中位数的结果样本不大当趋势看。指标A自建壳B终端壳说明任务完成率78%35/4581%37/45差距不显著失败集中在跨文件重构平均工具调用轮次11.39.6A 多出来的主要是探索轮次首轮工具选择准确率91%调优后94%A 调优前是 68%单任务相对成本0.211.00按各自计价口径折算长上下文任务成功率82%76%需要读 12 个以上文件的任务成本那一栏差得有点夸张但这是模型定价差异带来的跟壳本身没关系你换个模型这行数字就变了。我列出来是想说明一个事如果你要跑的是高频、低价值的任务比如批量补单测、批量改 import成本这个维度的权重应该比准确率高得多。差三四个点的准确率用人补一下的成本远低于十倍的调用成本。5.2 长上下文里的漂移问题长上下文那一项 A 领先 6 个点这个差异我专门拆开看过。原因是这样的B 的壳里有一套激进的历史裁剪策略跑到十几轮之后早期的探索结果会被压缩掉。大部分时候这没问题但当任务是先读十个文件搞清楚依赖再统一改这种模式时裁剪会把前面读到的关键信息剪没了模型就会回头重读或者更糟——凭印象瞎改。我抓到过一个典型caseB 在读第 9 个文件之后把第 2 个文件里定义的常量名记错了改出来的代码引用了不存在的符号。这个错误不是模型能力问题是它的上下文里那条信息已经被压掉了。A 这边是我自己控的上下文我在压缩策略上比较保守宁可多花点 token 也把关键结论留着。代价就是成本上去了、轮次多了。所以这个 6 个点的领先很大程度是壳的策略差异不能全算模型头上。这一点我觉得必须说清楚不然容易误导。5.3 中文注释与本地化生态这一项 A 的优势比较明显。我那个仓库里中文注释占比不低还有一些中文写的文档字符串。A 在理解这些内容、以及在生成新注释时保持风格一致上明显更自然。B 生成的中文注释语法没问题但那个味儿不太对像是翻译过来的术语选择也和项目里已有的不一致。这个差异在小项目上无所谓但在有严格代码规范、要求注释风格统一的团队里就会带来额外的 review 成本。我后来的做法是在系统提示词里塞两段项目里已有的注释样例让模型照着风格写这样两边都能拉到差不多的水平。所以这个优势也不是不可替代的只是 A 默认就更接近。5.4 成本账怎么算才不会被忽悠最后说说成本这块最容易踩的坑。很多人算成本只算 token 单价忽略了轮次带来的乘数效应。同样是完成一个任务A 用 11 轮B 用 9 轮但每一轮你都要把完整的历史重新发一遍。也就是说实际消耗的输入 token 大致是轮次的平方量级。11 轮和 9 轮输入 token 差了差不多 1.5 倍不是 1.2 倍。再加上失败重试的消耗。我的记录里失败任务的平均 token 消耗是成功任务的 2.3 倍因为它会反复试探、反复读文件。所以提升 3 个点的成功率省下来的成本可能比你想象的要多。这也是为什么我觉得工具描述的调优投入是划算的——它同时改善准确率和成本两个指标。6. 常见问题与排查技巧实录6.1 高频故障速查表下面这些是我一晚上加后续几天踩出来的按出现频率排。现象根因处理办法模型反复读同一个文件工具返回里含时间戳/随机 ID模型以为内容变了返回内容做稳定化去掉时间戳参数 JSON 解析失败流式分片没有按 index 累积按tc.index分组拼接再 loads改了文件但测试没变改了.pyc目录下的副本或改了错的同名文件路径必须 resolve工具返回绝对路径一直重试同一个失败命令错误信息太笼统模型不知道改什么错误信息里带上下一步该做什么任务做到一半开始跑偏上下文被压缩掉了关键结论只折叠工具结果保留 assistant 决策声称完成但没跑测试提示词里没强制验证系统提示词加验证不通过不许声称完成patch 匹配失败缩进/行尾空格不一致错误提示引导重新 read_file6.2 几个只有踩过才知道的细节第一个工具返回的第一行决定模型怎么理解结果。我一开始返回格式是结果...模型经常忽略。改成OK: ...或ERROR: ...开头之后它对成功失败的判断准确率高了一大截。这个改动小到可笑但效果是实打实的。第二个exit code一定要显式返回而且放在最前面。模型对数字比对人话敏感exit1比测试失败了更能让它停下来想想。我甚至试过把失败用例数放在第一行效果也不错。第三个别让模型自己决定什么时候停。我最早的设计是让模型输出一个done工具来表示完成结果它经常提前调用。后来改成模型不再输出工具调用就算结束加上轮次硬上限行为正常多了。人也是这样没有明确边界的时候容易自我感觉良好。第四个回放文件里的ms字段特别有用。我排查一个卡住的问题就是发现某个run_command花了 118 秒刚好卡在超时边缘原因是模型跑了个全量测试。看到这个数字之后我在提示词里加了优先跑相关测试文件而不是全量问题解决。第五个如果你的项目里有生成代码比如 protobuf、ORM 模型一定要在工具层把它们排除掉。模型看到生成文件里那一堆自动生成的类会试图去改它们改完下次生成又覆盖回去白忙一场。我在read_file里加了路径黑名单命中就返回这是生成文件源定义在 xxx省了大量无效探索。第六个temperature设 0.2 比设 0 好用。设 0 的时候模型偶尔会陷入完全重复的输出循环同一个错误答案连出三次。0.2 保留了一点点随机性能跳出死循环同时又不至于乱来。这个我试了好几组才定下来。第七个别信模型说它改了。一定要以工具返回和文件系统实际状态为准。我遇到过一次模型在回复里详细描述了它的修改但对应的apply_patch调用其实返回了 ERROR它没看就直接往下写了。这种时候只能靠回放里的工具结果来对账。所以 trace 落盘这件事再麻烦也得做。最后分享一个我最近在用的笨办法每次改完 Harness 的一处逻辑先拿三个固定的小任务跑一遍回归。这三个任务覆盖了读文件、改文件、跑测试三条主路径跑一遍不到两分钟。听起来很土但它帮我挡住了好几次改 A 坏 B的情况——尤其在你连续调 prompt 调到深夜的时候人的记忆是最不可靠的那一环。
企业数字化 ERP 产品动态
相关推荐
汽车CAD大文件上传优化:WebUploader分片与断点续传实践 1. 项目背景与需求解析 在汽车制造行业,CAD图纸的传输与协作是日常研发流程中的关键环节。我们团队最近遇到一个典型痛点:某车型的底盘总成CAD文件体积达到3.2GB,传统表单上传方式频繁出现超时失败。更棘手的是,网络波动导致的传输… · 2026/11/4 8:53:26
括号生成算法:递归回溯与动态规划详解 1. 括号生成问题解析 今天我们来探讨一个经典的算法问题——括号生成。给定一个数字n,要求生成所有可能的并且有效的括号组合。比如当n3时,有效的组合有:["((()))","(()())","(())()","()(())",&quo… · 2026/11/4 8:53:26
C++中rapidjson组装map和数组array的代码示例 前言
把内存中的 std::map、std::vector 之类容器序列化成 JSON,是服务端和配置读写里最常见的操作之一。rapidjson 是一个只有头文件的 C JSON 库,解析和生成都快、依赖少,但它的对象模型和「随手拼字符串」很不一样:所有值都挂在… · 2026/11/4 8:53:26
LangGraph入门:状态图、条件路由与Agent工具调用循环 最近连续有几个人跑来问我同一个问题:LangGraph 和 LangChain 到底什么区别?问得多了,我发现大家不是不懂用法,而是卡在了同一个点上——链式调用写多了之后,根本想象不出"图"怎么组织 Agent。这篇就围绕标题… · 2026/11/4 9:24:26
NPS增强版内网穿透部署与优化实战指南 1. 项目概述:从原版NPS到增强版的技术迁移 作为一名长期从事网络运维的工程师,我亲历了原版NPS(ehang-io/nps)从活跃到停滞的整个过程。这个曾经广受欢迎的内网穿透工具,由于长期缺乏维护(最后一次提交停留… · 2026/11/4 9:24:26
CentOS7下OpenSSL生成CA根证书全流程详解 1. CentOS7环境下CA根证书生成全流程解析在Linux服务器运维和分布式系统部署中,证书颁发机构(CA)的建立是构建可信安全体系的基础环节。作为在金融行业浸淫多年的系统架构师,我经常需要为内部系统搭建私有CA。本文将详细记录在CentOS7环境下从零开始建立… · 2026/11/4 9:24:26
美团Hera分布式监控系统:微服务全链路追踪实战 1. 分布式监控系统Hera的核心价值在微服务架构成为主流的今天,系统复杂度呈指数级增长。记得去年我们团队遇到一个棘手的生产问题:用户投诉订单支付成功率突然下降,但各个服务日志都没有明显异常。整整两天时间,我们像无头苍蝇一样… · 2026/11/4 9:24:26
STM32硬件SPI驱动AT45DB161D实战:时序匹配、忙等待与页擦除避坑指南 简介:本资源是一套基于STM32硬件SPI驱动AT45DB161D串行Flash的完整嵌入式开发工程,面向嵌入式初学者与STM32进阶开发者,解决外置大容量非易失存储器在ARM Cortex-M平台上的可靠读写难题。工程适配红牛STM32F103ZET6开发板,涵盖SPI… · 2026/11/4 9:24:26
LangChain LLM应用缓存实战:从普通缓存到语义缓存,降本提速 上个月我盯了一眼项目账单,某一天的日志里同一个问题被问了几百次,扣费记录翻了好几页。那一刻我突然意识到一件事:很多团队花大力气优化 prompt、压缩上下文、换更便宜的模型,却漏掉了最基础也最直接的一招——缓存。 这篇文章是… · 2026/11/4 9:23:25
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 6:59:29
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 7:02:38