从0打造编码Agent 阅读 + 实验 ≈ 30 分钟 页内实验 ×1
A2 工具系统:模型怎么说清「改哪里」? 第 3 课

能不能从源头保证 patch 格式合法?

让格式从源头合法
在 prompt 里教格式,模型仍会写错:aider 榜上 diff 格式的「格式正确」最低只有 64.4%,Codex 的解析器也为模型放宽了规则。Codex 的做法是把 19 行 Lark 语法交给服务端,解码时就把「会让格式不合法」的 token 挡掉。它保证的只是语法:定位仍靠 seek_sequence,两处相同的行照样改到第一处。
❓

本课一问:第 2 课的引擎都在收拾「模型抄错了」的残局。Codex 走的是另一条路:让模型写一种专门的 patch 格式。可格式本身也会写错——能不能在模型生成的时候,就保证它写出来的一定合法?

这一课先不碰:patch 写对之后怎么在文件里定位(第 2 课的 seek_sequence);边生成边解析的流式 patch(Codex 的 streaming_parser.rs,A8 讲流式时再看);外部工具(第 4 课)。

从已有出发

A1 说过:模型只会生成文字,工具调用也是文字。本课项目的真实请求里能直接看到——pd-truncate-s1 第 1 轮模型回的那条调用,经 Qwen3 的 chat template 渲染后就是这样一段文本:

<tool_call>
{"name": "list_dir", "arguments": {"path": "."}}
</tool_call>
从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过

服务端的解析器把 tool_call 标签里的 JSON 切出来,变成结构化的 tool_calls 字段。JSON 这种格式模型训练时见得多:本课项目迄今 26 次运行、139 次工具调用,参数解析失败 0 次(项目 trace 统计,单一模型、样本小)。

patch 不一样。Codex 的 apply_patch 要求模型写出下面这种格式——既不是 JSON,也不是 git 的 unified diff,而是 Codex 自己定义的:

*** Begin Patch
*** Update File: dateparse.py
@@ def parse_date(text):
-        month, day, year = (int(g) for g in m.groups())
+        day, month, year = (int(g) for g in m.groups())
*** End Patch

先自己设计:在 prompt 里把格式教清楚

最直接的办法是写一份说明书放进 prompt。Codex 仓库里就保留着这样一份,75 行,给不支持语法约束的模型用:

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案在 prompt 里教格式,模型照着写
apply_patch_tool_instructions.md(纯 prompt 版说明,共 75 行) codex@8cabf5a:codex-rs/prompts/templates/apply_patch_tool_instructions.md:1-24需联网 Apache-2.0
1## `apply_patch`
2
3Use the `apply_patch` shell command to edit files.
4Your patch language is a stripped‑down, file‑oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high‑level envelope:
5
6*** Begin Patch
7[ one or more file sections ]
8*** End Patch
9
10Within that envelope, you get a sequence of file operations.
11You MUST include a header to specify the action you are taking.
12Each operation starts with one of three headers:
13
14*** Add File: <path> - create a new file. Every following line is a + line (the initial contents).
15*** Delete File: <path> - remove an existing file. Nothing follows.
16*** Update File: <path> - patch an existing file in place (optionally with a rename).
17
18May be immediately followed by *** Move to: <new path> if you want to rename the file.
19Then one or more “hunks”, each introduced by @@ (optionally followed by a hunk header).
20Within a hunk each line starts with:
21
22For instructions on [context_before] and [context_after]:
23- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first change’s [context_after] lines in the second change’s [context_before] lines.
24- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
解析器为模型放宽规则 codex@8cabf5a:codex-rs/apply-patch/src/parser.rs:46-53需联网 Apache-2.0
46
47/// Currently, the only OpenAI model that knowingly requires lenient parsing is
48/// gpt-4.1. While we could try to require everyone to pass in a strictness
49/// param when invoking apply_patch, it is a pain to thread it through all of
50/// the call sites, so we resign ourselves allowing lenient parsing for all
51/// models. See [`ParseMode::Lenient`] for details on the exceptions we make for
52/// gpt-4.1.
53const PARSE_IN_STRICT_MODE: bool = false;
👆 带下划线的行可以点:点哪行,讲哪行。

同一个模型换一种更简单的格式,错得就少:aider 榜上同一个 Qwen2.5-Coder-32B-Instruct,whole 格式 99.6%,diff 格式 71.6%(第 1 课的图)。格式越复杂,靠 prompt 教就越不稳。

aider 排行榜:模型按哪种 edit 格式回复,「格式正确」的比例(每个点一个模型,69 个条目)
60%70%80%90%100% wholediff-fenceddiffarchitect Gemini 2.0 Pro exp-02-05 · whole · 格式正确 100% · 两次尝试通过率 35.6% · 2025-02-25 gpt-4o-mini-2024-07-18 · whole · 格式正确 100% · 两次尝试通过率 3.6% · 2024-12-21 claude-3-5-sonnet-20241022 · diff · 格式正确 99.6% · 两次尝试通过率 51.6% · 2025-01-17 gpt-4o-2024-11-20 · diff · 格式正确 95.1% · 两次尝试通过率 18.2% · 2024-12-30 gpt-4o-2024-08-06 · diff · 格式正确 94.2% · 两次尝试通过率 23.1% · 2024-12-30 o1-2024-12-17 (high) · diff · 格式正确 91.5% · 两次尝试通过率 61.7% · 2024-12-21 DeepSeek Chat V2.5 · diff · 格式正确 92.9% · 两次尝试通过率 17.8% · 2024-12-21 claude-3-5-haiku-20241022 · diff · 格式正确 91.1% · 两次尝试通过率 28% · 2024-12-21 Qwen2.5-Coder-32B-Instruct · diff · 格式正确 71.6% · 两次尝试通过率 8% · 2024-12-22 o1-mini-2024-09-12 · whole · 格式正确 96.9% · 两次尝试通过率 32.9% · 2024-12-22 gemini-exp-1206 · whole · 格式正确 98.2% · 两次尝试通过率 38.2% · 2024-12-22 gemini-2.0-flash-exp · whole · 格式正确 100% · 两次尝试通过率 22.2% · 2024-12-22 yi-lightning · whole · 格式正确 92.9% · 两次尝试通过率 12.9% · 2024-12-23 DeepSeek Chat V3 (prev) · diff · 格式正确 98.7% · 两次尝试通过率 48.4% · 2024-12-25 Qwen2.5-Coder-32B-Instruct · whole · 格式正确 99.6% · 两次尝试通过率 16.4% · 2024-12-26 Codestral 25.01 · whole · 格式正确 100% · 两次尝试通过率 11.1% · 2025-01-13 DeepSeek R1 · diff · 格式正确 96.9% · 两次尝试通过率 56.9% · 2025-01-20 DeepSeek R1 + claude-3-5-sonnet-20241022 · architect · 格式正确 100% · 两次尝试通过率 64% · 2025-01-23 qwen-max-2025-01-25 · diff · 格式正确 90.2% · 两次尝试通过率 21.8% · 2025-01-28 o3-mini (medium) · diff · 格式正确 95.1% · 两次尝试通过率 53.8% · 2025-01-31 o3-mini (high) · diff · 格式正确 93.3% · 两次尝试通过率 60.4% · 2025-01-31 gemini-2.0-flash-thinking-exp-01-21 · diff · 格式正确 77.8% · 两次尝试通过率 18.2% · 2025-01-21 chatgpt-4o-latest (2025-02-15) · diff · 格式正确 93.3% · 两次尝试通过率 27.1% · 2025-02-15 claude-3-7-sonnet-20250219 (no thinking) · diff · 格式正确 93.3% · 两次尝试通过率 60.4% · 2025-02-24 claude-3-7-sonnet-20250219 (32k thinking tokens) · diff · 格式正确 97.8% · 两次尝试通过率 64.9% · 2025-02-24 gpt-4.5-preview · diff · 格式正确 97.3% · 两次尝试通过率 44.9% · 2025-02-27 QwQ-32B · diff · 格式正确 67.6% · 两次尝试通过率 20.9% · 2025-03-06 QwQ-32B + Qwen 2.5 Coder Instruct · architect · 格式正确 100% · 两次尝试通过率 26.2% · 2025-03-07 command-a-03-2025-quality · whole · 格式正确 99.6% · 两次尝试通过率 12% · 2025-03-14 gemma-3-27b-it · whole · 格式正确 100% · 两次尝试通过率 4.9% · 2025-03-15 DeepSeek V3 (0324) · diff · 格式正确 99.6% · 两次尝试通过率 55.1% · 2025-03-24 Gemini 2.5 Pro Preview 03-25 · diff-fenced · 格式正确 92.4% · 两次尝试通过率 72.9% · 2025-04-12 chatgpt-4o-latest (2025-03-29) · diff · 格式正确 64.4% · 两次尝试通过率 45.3% · 2025-03-29 Quasar Alpha · diff · 格式正确 98.2% · 两次尝试通过率 54.7% · 2025-04-04 Llama 4 Maverick · whole · 格式正确 99.1% · 两次尝试通过率 15.6% · 2025-04-06 Grok 3 Beta · diff · 格式正确 99.6% · 两次尝试通过率 53.3% · 2025-04-10 Grok 3 Mini Beta (low) · whole · 格式正确 100% · 两次尝试通过率 34.7% · 2025-04-10 Grok 3 Mini Beta (high) · whole · 格式正确 99.6% · 两次尝试通过率 49.3% · 2025-04-10 Optimus Alpha · diff · 格式正确 97.3% · 两次尝试通过率 52.9% · 2025-04-10 gpt-4.1 · diff · 格式正确 98.2% · 两次尝试通过率 52.4% · 2025-04-14 gpt-4.1-mini · diff · 格式正确 92.4% · 两次尝试通过率 32.4% · 2025-04-14 gpt-4.1-nano · whole · 格式正确 94.2% · 两次尝试通过率 8.9% · 2025-04-14 o4-mini (high) · diff · 格式正确 90.7% · 两次尝试通过率 72% · 2025-04-16 openhands-lm-32b-v0.1 · whole · 格式正确 95.1% · 两次尝试通过率 10.2% · 2025-04-19 gemini-2.5-flash-preview-04-17 (default) · diff · 格式正确 85.3% · 两次尝试通过率 47.1% · 2025-04-20 Gemini 2.5 Pro Preview 05-06 · diff-fenced · 格式正确 97.3% · 两次尝试通过率 76.9% · 2025-05-07 Qwen3 32B · diff · 格式正确 83.6% · 两次尝试通过率 40% · 2025-05-08 Qwen3 235B A22B diff, no think, Alibaba API · diff · 格式正确 92.9% · 两次尝试通过率 59.6% · 2025-05-09 claude-sonnet-4-20250514 (no thinking) · diff · 格式正确 98.2% · 两次尝试通过率 56.4% · 2025-05-24 claude-sonnet-4-20250514 (32k thinking) · diff · 格式正确 97.3% · 两次尝试通过率 61.3% · 2025-05-24 claude-opus-4-20250514 (no think) · diff · 格式正确 98.7% · 两次尝试通过率 70.7% · 2025-05-25 claude-opus-4-20250514 (32k thinking) · diff · 格式正确 97.3% · 两次尝试通过率 72% · 2025-05-25 gemini-2.5-flash-preview-05-20 (no think) · diff · 格式正确 93.8% · 两次尝试通过率 44% · 2025-05-26 gemini-2.5-flash-preview-05-20 (24k think) · diff · 格式正确 95.6% · 两次尝试通过率 55.1% · 2025-05-25 gemini-2.5-pro-preview-06-05 (default think) · diff-fenced · 格式正确 100% · 两次尝试通过率 79.1% · 2025-06-06 gemini-2.5-pro-preview-06-05 (32k think) · diff-fenced · 格式正确 99.6% · 两次尝试通过率 83.1% · 2025-06-06 DeepSeek R1 (0528) · diff · 格式正确 94.6% · 两次尝试通过率 71.4% · 2025-06-06 o3 (high) · diff · 格式正确 94.7% · 两次尝试通过率 81.3% · 2025-06-25 o3 · diff · 格式正确 93.8% · 两次尝试通过率 76.9% · 2025-06-25 o3 (high) + gpt-4.1 · architect · 格式正确 100% · 两次尝试通过率 78.2% · 2025-06-27 o3-pro (high) · diff · 格式正确 97.8% · 两次尝试通过率 84.9% · 2025-06-28 grok-4 (high) · diff · 格式正确 97.3% · 两次尝试通过率 79.6% · 2025-07-11 Kimi K2 · diff · 格式正确 92.9% · 两次尝试通过率 59.1% · 2025-07-17 gpt-oss-120b (high) · diff · 格式正确 79.1% · 两次尝试通过率 41.8% · 2025-08-06 gpt-5 (high) · diff · 格式正确 91.6% · 两次尝试通过率 88% · 2025-08-23 gpt-5 (medium) · diff · 格式正确 88.4% · 两次尝试通过率 86.7% · 2025-08-25 gpt-5 (low) · diff · 格式正确 86.7% · 两次尝试通过率 81.3% · 2025-08-25 DeepSeek-V3.2-Exp (Reasoner) · diff · 格式正确 97.3% · 两次尝试通过率 74.2% · 2025-10-03 DeepSeek-V3.2-Exp (Chat) · diff · 格式正确 98.2% · 两次尝试通过率 70.2% · 2025-10-03 71.6%99.6%
格式条目最低中位数
whole1592.9%99.6%
diff-fenced492.4%98.45%
diff4764.4%94.2%
architect3100%100%

高亮的一对是同一个模型 Qwen2.5-Coder-32B-Instruct 分别用 diff 与 whole 格式跑同一套题。数据:Aider-AI/aider 本地副本 @ cb6a152e 2025-10-04 11:14:08 +0200(aider/website/_data/polyglot_leaderboard.yml、edit_leaderboard.yml);字段 percent_cases_well_formed(线上页面列名「Correct edit format」,2025-11-20 更新,2026-09-26 核对可访问)。官方自报、单次运行;「格式正确」是 aider 自己的判定口径,不等于改对了代码。

改进:语法约束解码

Codex 把语法交给服务端的那几行:

apply_patch_spec.rs codex@8cabf5a:codex-rs/core/src/tools/handlers/apply_patch_spec.rs:5-28需联网 Apache-2.0
5const APPLY_PATCH_LARK_GRAMMAR: &str = include_str!("apply_patch.lark");
6
7/// Returns a custom tool that can be used to edit files. Well-suited for GPT-5 models
8/// https://platform.openai.com/docs/guides/function-calling#custom-tools
9pub fn create_apply_patch_freeform_tool(include_environment_id: bool) -> ToolSpec {
10 let definition = if include_environment_id {
11 APPLY_PATCH_LARK_GRAMMAR.replace(
12 "start: begin_patch hunk+ end_patch",
13 "start: begin_patch environment_id? hunk+ end_patch\nenvironment_id: \"*** Environment ID: \" filename LF",
14 )
15 } else {
16 APPLY_PATCH_LARK_GRAMMAR.to_string()
17 };
18 ToolSpec::Freeform(FreeformTool {
19 name: "apply_patch".to_string(),
20 description: "The `apply_patch` tool can be used to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.".to_string(),
21 defer_loading: None,
22 format: FreeformToolFormat {
23 r#type: "grammar".to_string(),
24 syntax: "lark".to_string(),
25 definition,
26 },
27 })
28}
👆 带下划线的行可以点:点哪行,讲哪行。

OpenAI 的文档写明了服务端怎么用它 :自定义工具可以带一份上下文无关文法,目前支持 lark 与 regex 两种写法,采样过程由 LLGuidance 约束。

这不是 OpenAI 独有的思路。Claude 的工具定义可以设 strict: true,官方文档的说法是它「constraining the model’s token sampling to schema-valid outputs」,保证工具名与参数符合 JSON Schema 。Pi 从 0.86.0 起内置工具默认优先用这种 JSON Schema 约束采样(勘误表 P8,pin 31b513e3 之后的新版本)。

在浏览器里看「解码器看到的东西」

下面这个组件把那 19 行语法写成了一个按行推进的检查器:每输入一个字符,它回答「这还是不是某个合法 patch 的开头」,以及「下一个字符允许是什么」——后一个问题,正是约束解码每一步要回答的。

apply_patch 的 19 行 Lark 语法 · 逐字检查前缀

语法原文:openai/codex@8cabf5a apply_patch.lark(最新版移到 core/assets/tools/,内容逐字相同,见勘误表 X3)。本组件把语法写成按行推进的状态机;参照答案由 gen_a02_fixtures.py 把同一份语法机械转写成 Python 正则算出(本机未装 lark,python-re:按 apply_patch.lark 19 行逐条转写(完整串 + 前缀闭包))。它只检查「字符串属不属于这门语言」,不检查 patch 能不能在文件上找到位置——那是 seek_sequence 的事。

🎯

试三件事:① 先猜「模型写成了 unified diff」会在第几个字符被拒,再点它:第 1 个字符 - 就不行,此刻只允许 *;② 点「像解码器一样逐字回放」看合法 patch:每一步下面那排字符里,绿色的才是解码器允许采样的;③ 点「@@ 后面空着」和「Windows 换行」:前者卡在 @@ 之后必须至少一个字符,后者卡在 \r——语法里的换行只认 \n。

19 行语法本身也值得逐行读:

apply_patch.lark codex@8cabf5a:codex-rs/core/src/tools/handlers/apply_patch.lark:1-19需联网 Apache-2.0 最新版已变化 · 勘误表 X3
1start: begin_patch hunk+ end_patch
2begin_patch: "*** Begin Patch" LF
3end_patch: "*** End Patch" LF?
4
5hunk: add_hunk | delete_hunk | update_hunk
6add_hunk: "*** Add File: " filename LF add_line+
7delete_hunk: "*** Delete File: " filename LF
8update_hunk: "*** Update File: " filename LF change_move? change?
9
10filename: /(.+)/
11add_line: "+" /(.*)/ LF -> line
12
13change_move: "*** Move to: " filename LF
14change: (change_context | change_line)+ eof_line?
15change_context: ("@@" | "@@ " /(.+)/) LF
16change_line: ("+" | "-" | " ") /(.*)/ LF
17eof_line: "*** End of File" LF
18
19%import common.LF
👆 带下划线的行可以点:点哪行,讲哪行。
⚠
常见误解:「用了约束解码,模型就更会改代码了。」

它只保证「写出来的是一个语法合法的 patch」。patch 在文件里落在哪,仍由第 2 课的 seek_sequence 决定:它按整行比较、不检查唯一、取第一处。第 2 课的真实用例在 Codex 上照样出问题——pd-truncate-s1 第 4 轮那行在文件里有两处,Codex 改到了第 31 行的 ISO 分支,写回测试 FAILED (failures=2);pd-unlimited-s3 第 4 轮的 old_string 没写缩进,Codex 在「忽略首尾空白」这一级接住,再把没缩进的新行原样写进去,结果是 IndentationError(Codex 一栏的输入是把 old/new 按行转写成 patch,见第 2 课的说明)。

🔍 Codex 为什么在 patch 里要求上下文行和 @@?

正因为 seek_sequence 取第一处。纯 prompt 版说明(上面 75 行那份)要求默认带上下 3 行上下文,3 行不够唯一时再用 @@ 写出所属的 class 或函数名;compute_replacements 会先用 @@ 那一行把搜索起点推到它后面(file_update.rs:94-107),再找变更块。也就是说,「唯一」这件事被交还给了模型:它要自己写够上下文。约束解码能保证 @@ 后面有文字,保证不了那段文字足够把位置钉死。

证据三问

问本课的回答
① 它解决什么问题?自定义格式靠 prompt 教不稳:格式错了,harness 只能报错或宽松解析
② 为什么可能有效?每一步只允许「仍可能合法」的 token,最终输出必在语法之内 ;它不改变模型「想改哪里」
③ 实验上呢?靠 prompt 时格式错误率不为零:aider 榜 diff 类格式「格式正确」最低 64.4% ;用约束解码之后格式错误率降到多少,本轮没有找到公开的对照数据,只有厂商的机制说明

演进地图:你在这里

模型怎么说清「改哪里」:每一代都被上一代的某个真数字逼出来
  1. 朴素 按行号改 「把第 a–b 行换成……」
    解决了模型不用复述原文,只报行号,输出最短。
    卡在行号是「上一次读文件时」的快照:第一处修复只要多插 2 行,第二次按旧行号 41 改,就落在 if m: 上(第 1 课现算);CoreCoder README 的原话是「Line numbers are a trap」。
  2. 第一代 整文件覆写 write_file;aider 的 whole 格式
    解决了不需要定位,格式最不容易错:aider 同一个 Qwen2.5-Coder-32B,whole 格式合规 99.6%,diff 格式只有 71.6%。
    卡在只改 1 行,也要把 44 行的文件整个再写一遍:参数 487 token,片段替换只要 54(9.0 倍,项目模型 tokenizer 实数);还会覆盖别人在此期间的改动。
  3. 第二代 唯一片段替换(search-replace) CoreCoder、本课项目 miniagent
    解决了按内容定位,不怕行号过期;0 处或多处都报错、文件不动,让模型自己补上下文。
    卡在真实 trace pd-truncate-s1:第 4 轮 old_string 在文件里 2 处、第 5 轮缩进多 4 格 0 处,两次报错多跑了两轮,花掉 8,481 token、109.5 秒;唯一也不等于对:pd-v2-unlimited-s1 第 7 轮的 old_string 恰好是已修好那一行的前半截,计数 = 1,于是 + 1 变成了 + 1 + 1。
  4. 解决了模型复述时的小漂移不再白费一轮:上面第 5 轮那条缩进多 4 格的 edit,OpenCode 直接接住(LineTrimmed 级)。
    卡在接住 ≠ 改对:多转义一层的引号,OpenCode 的 EscapeNormalized 级照样接住,写回去的是 \",模块导入即 SyntaxError;OpenCode 的 v2(packages/core)眼下只有精确匹配,「edit fuzzy parity」还列在待移植清单里。
  5. 第四代 语法约束解码的 patch Codex apply_patch(19 行 Lark 语法);strict 工具 你在这里
    解决了语法由解码器保证,不再靠 prompt 教。对照:靠 prompt 教格式时,aider 榜上 47 个 diff 格式条目的「格式正确」比例最低只有 64.4%(官方自报)。
    卡在语法只管格式,定位仍是 seek_sequence:它不检查唯一、取第一处——两处相同的 return datetime.date(year, month, day) 会改到第 31 行 ISO 分支,test_parse_iso 跟着坏;整行比较还会把没写缩进的 new 行原样写进去,得到 IndentationError。
  6. 工具从哪来 工具集随模型变,外部工具经 MCP 接入 OpenCode registry.ts;MCP 2026-07-28
    解决了一套协议接任意外部工具;同一个 harness 给 GPT 换上 apply_patch、给 Claude 用 edit。
    卡在接进来的每个工具每轮都要付上下文:chrome-devtools-mcp 的 30 个工具在项目模型的 chat template 里占 7,047 token,是 miniagent 自己 6 个工具(685)的 10.3 倍。
  7. 岔路 Code Mode:让模型写脚本调工具 Codex exec / wait;OpenCode codemode
    解决了N 次「模型 → 工具 → 模型」往返压成一段脚本。
    卡在省的是往返,不省判断:真实 trace 里模型每一步都要看上一步的结果才知道下一步做什么(岔路课逐轮算)。
数字口径:token 数用项目录制模型 Qwen3-8B 的 tokenizer 与 chat template 算(与服务端 usage 逐轮核对一致);裁决与测试结果是原仓库代码实跑(a02-edit-fixtures.json,2026-09-26);trace 来自本课项目 miniagent 的真实运行(2026-09-26);aider 数字来自 aider 仓库的 polyglot_leaderboard.yml(官方自报);MCP 工具数来自本机实跑 tools/list。

一句话

📌
能用解码约束保证的格式,就别靠 prompt 教;约束只管格式,不管定位。
🚦

先停在这里:分清两件事:「写得合法」可以由解码器保证,「改对地方」不行。
再深一层:在检查器里自己写一个 patch,故意漏掉 *** End Patch,看状态停在「合法前缀,还没写完」——约束解码会一直不让模型结束,直到它补上。

下一个缺口

内置工具之外,外部工具怎么接进来? → 第 4 课:外部工具怎么接进来?MCP 放在哪一层?

到这里,工具都是 harness 自己写的:read、edit、bash……可用户还想接数据库、浏览器、公司内部的系统。第 4 课看外部工具怎么进来,以及每接一个工具,每一轮要多付多少上下文。

你可能会问