OLLAMA 本地模型不支持 tool_call 的根因与等效替代方案
本仓 experiment_modules/solving/adapters/ollama/ 下三个 runner —— ollama-one-shot / ollama-pipeline / ollama-shell —— 在调用 Ollama OpenAI 兼容端点时均不传 tools 字段。OpenAI 协议明明定义了 tools / tool_calls / role:"tool","正常 Agent"按理应走这条路——为什么本仓偏不?这不是一个"模型弱就放弃"的偷懒决定,而是一个经过三层叠加分析 + 任务形态分析后做出的工程选择。 这一页是一个完整的工程决策复盘,论证链 + 失败模式 + 替代实现 + 选型决策树都摊开。
概览
| 项目 | 说明 |
|---|---|
| 本卡定位 | 工程决策复盘 · 6 卡片从"为什么"到"怎么绕"到"何时改回" |
| 前置知识 | 读过 llm-api-schema-reference.html 卡片 4(工具调用基础)/ llm-chat-protocol-guide.html §8(兼容方言陷阱) |
| 读完会什么 | 能用 L1/L2/L3 心智模型分析任何"协议层 vs 兼容层 vs 模型层"的工程问题;能解释为什么 ollama 三个 runner 不传 tools 字段;能用决策树选型 |
| 本仓对应 | experiment_modules/solving/adapters/ollama/{one_shot,pipeline,shell}_runner.py |
| 源报告 | docs/REPORT-ollama-tool-call-substitute-20260902.md(v1.1) |
第一讲 · 破题:决策全貌与三层叠加
本报告回答的 3 个问题 + L1/L2/L3 心智模型
这个决策不是"模型弱所以放弃 tool_call"那么简单——它经过三层叠加 + 任务形态两层分析才得出的工程结论。记住这个三层叠加:协议层(L1)支持 ≠ 兼容层(L2)能用 ≠ 模型层(L3)会发。任何"为什么 XX 不能用标准协议"的问题,都可以套这个分层去拆。
- 为什么 ollama 三个 runner 不走标准 tool_call 协议?(§1-3 回答:三层叠加 + 任务形态)
- 三个 runner 如何等效实现 tool_call 该承担的能力?(§4 回答:范式转移 + 能力对照表)
- 在哪些场景下应改回标准 tool_call?(§5 回答:决策树 + 4 场景行动项)
"三层叠加"是这个报告的核心方法论。先对齐一个基础词:本页反复出现的 API(Application Programming Interface,应用程序接口)指"调用大模型服务的 HTTP 接口"。在看图之前,先把每一层是什么 / 提供什么 / 在 ollama 场景的具体表现 / 可能怎么崩讲清楚——后面所有论证都站在这层之上。
- 是什么:由 OpenAI 等规范方定义的协议标准——tools / tool_choice / message.tool_calls / role:"tool" + tool_call_id 四个字段在文档里写得清清楚楚
- 提供什么:一份"客户端与推理服务之间应该传什么 JSON(JavaScript Object Notation,人类可读的文本数据格式)"的合同——所有兼容端点都声明支持
- 在 ollama 场景:Ollama 0.5+ 文档 https://github.com/ollama/ollama/blob/main/docs/openai.md 明确写了支持 /v1/chat/completions 兼容端点,包括 tools
- 怎么崩:基本不会崩——L1 是"标准在不在"的问题。OpenAI 2023-06 起就把这套 API 钉死了
- 是什么:各个推理服务(Ollama / vLLM / DeepSeek / DashScope / Together / Groq 等)自己实现的 OpenAI 兼容端点——它们读规范、写代码、暴露 HTTP 端口
- 提供什么:一个"长得像 OpenAI API"的端点——但只是"长得像",行为未必一致
- 在 ollama 场景:Ollama 0.5.x 之前对未知 / 不完整字段的策略是静默忽略而非报错(tool_choice:"required" 部分版本失效、strict:true 静默忽略等)
- 怎么崩:"兼容端点不等于行为一致"——L1 通过不代表 L2 通过。这是 OpenAI cookbook 反复警告过的"方言陷阱"(详见姊妹讲义 llm-chat-protocol-guide.html §8)
- 是什么:模型本身在训练数据里学到的能力——能不能稳定输出 tool_call JSON、能不能写出符合 JSON Schema 的参数、能不能在正确位置填 id
- 提供什么:模型推理的"格式技能"——这是模型权重决定的,不是 L1/L2 能改的
- 在 ollama 场景:本机实测的 qwen3.8:27b-mlx / qwen3.6:35b-coding-mxfp8 是 coding 任务微调,没做 tool_call SFT——发不出 tool_call JSON
- 怎么崩:训练数据决定能力边界——换 prompt 没用、换 API 没用、换兼容层没用,必须换模型(或换带 function-calling 微调的版本,如 qwen2.5:7b-instruct、nous-hermes:function-calling)
| 层 | 谁来定 | 提供什么 | ollama 场景的具体表现 | 失败模式 |
|---|---|---|---|---|
| L1 协议层 | OpenAI 等规范方 | API 字段的合同定义 | 工具调用 4 字段在文档里写得清清楚楚 | 基本不崩;2023-06 起稳定 |
| L2 兼容层 | Ollama / vLLM / DeepSeek / DashScope 等 | "长得像 OpenAI"的端点 | 对未知字段静默忽略而非报错 | 方言陷阱:"兼容 ≠ 行为一致" |
| L3 模型层 | 模型训练数据(预训练 + SFT + RLHF) | 输出 tool_call JSON 的格式技能 | coding 模型没做 tool_call SFT,发不出 JSON | 换啥都救不了,只能换模型 |
标准 OpenAI Chat Completions
定义 tools / tool_choice / tool_calls 字段"] L2["L2 兼容层
Ollama / vLLM / DeepSeek / DashScope
「OpenAI 兼容端点」的方言实现"] L3["L3 模型能力层
模型本身的训练数据 / SFT 决定
能不能稳定输出 tool_call JSON"] L1 -->|协议层支持| L2 L2 -->|兼容层能用| L3 L3 -->|模型会发| Result["实际能否 tool_call"] L1 -.->|L1 通过 不等于 端到端可用| Trap1["协议陷阱"] L2 -.->|L2 通过 不等于 模型能调| Trap2["方言陷阱"] L3 -.->|L3 通过 不等于 弱模型不崩| Trap3["训练数据陷阱"]
- 代码审计者:评估"为什么 ollama runner 源码里没传 tools"——看完能讲清"这不是偷懒,是工程权衡"
- 架构决策者:决定"该上 ollama 还是云端 API"——看完能用决策树选型
- 新 agent 开发者:准备接入新 agent 类型——看完知道"何时该传 tools,何时不该传"
第二讲 · 协议基线:标准 tool_call 长什么样
OpenAI Chat Completions 的 tool_call:4 字段 + 完整往返
tool_call 是 OpenAI 协议中"让模型发起工具调用"的标准机制。涉及 4 个标准字段:请求体 tools / tool_choice,响应体 message.tool_calls,消息回填 role:"tool" + tool_call_id。"正常 Agent"全部走这条路——但这条路对 LLM(Large Language Model,大语言模型)输出的"格式技能"要求很高。
| 字段 | 位置 | 类型 | 作用 |
|---|---|---|---|
| tools | 请求体顶层 | ToolDefinition[] | 工具定义列表(含 JSON Schema) |
| tool_choice | 请求体顶层 | "auto" | "none" | "required" | {type:"function", function:{name}} | 控制模型是否必须调 / 调哪个 |
| message.tool_calls | 响应 choices[0].message | ToolCall[] | 模型发起的工具调用 |
| role:"tool" + tool_call_id | 消息数组 | 单条消息 | 工具结果回填,与 tool_calls[].id 严格配对 |
① Agent 组装 messages + tools ──→ POST /v1/chat/completions
② 解析响应,读 finish_reason
│
├── "stop" → 收尾退出
│
└── "tool_calls" → 逐个执行:
- json.loads(arguments) ← 防御 try/except
- 本地执行 shell/函数/HTTP
- 取 stdout
- append {"role":"tool", tool_call_id, content}
→ 回到 ①(携带工具结果再请求)
{
id: string, // "call_..." 配对键
type: "function",
function: {
name: string,
arguments: string // ⚠️ JSON 字符串,不是对象!需 json.loads + 防御
}
}
1. 语义层:决定调哪个工具
2. 结构层:生成符合 JSON Schema 的参数
3. 语法层:在正确位置填入 id / type:"function" 字段
三层都"必须"模型在 SFT 阶段见过大量"问题 → tool_call JSON"样本对才能稳定输出。这是卡片 3 L3 模型能力层的伏笔——没有这种 SFT 的模型,发不出 tool_call JSON。
第三讲 · 为什么不传 tools:L1/L2/L3 三层叠加拆解
三层叠加:协议层标准 ≠ 兼容层能用 ≠ 模型层会发
本节是报告的核心论证章。"不传 tools"不是单一原因——是协议层(L1)支持 + 兼容层(L2)方言陷阱 + 模型层(L3)训练数据限制三层叠加的工程结论。三层都"通过"才真能用;任一层薄弱,端到端仍崩。
所有宣称"OpenAI 兼容"的 chat completions 端点(Ollama / vLLM / DeepSeek / DashScope / Together / Groq 等)都声明支持 tools 字段。OpenAI Chat Completions v2023-06 起即稳定,tools / tool_choice / message.tool_calls / role:"tool" + tool_call_id 全部标准字段定义清晰。
Ollama 0.5.x 之前对未知 / 不完整字段的策略是静默忽略而非报错:
- tool_choice: "required" 或指定函数形式:部分版本失效
- 旧版本对 tools 数组里的 strict: true 字段:静默忽略
- 工具定义里 parameters.strict: true 强约束模式:取决于版本
加上本仓 one_shot_runner.py D4 决策记录的血泪事实:
—— "兼容端点不等于行为一致"是 L2 的核心陷阱。
根本原因:模型输出 tool_call JSON 的能力完全由训练数据决定。
| 训练阶段 | 训练内容 | 学到什么 |
|---|---|---|
| 预训练(Pre-training) | TB 级网页 / 代码 / 书籍 | 语言模式、续写、bash 命令 |
| 监督微调(SFT) | 万-百万级指令-响应对 | 按指令回答、输出特定格式(含 tool_call JSON) |
| RLHF / DPO | 人类偏好标注 | 答得更像人、更稳定 |
tool_call JSON 是一种"格式技能",必须在 SFT 阶段见过大量"问题 → tool_call JSON"样本对才能稳定输出。
| 模型 | 训练定位 | tool_call SFT? | 表现 |
|---|---|---|---|
| qwen3.8:27b-mlx | SWE-bench 风格 coding 任务 | ❌ 无 | 不会 tool_call |
| qwen3.6:35b-coding-mxfp8 | SWE-bench 风格 coding 任务 | ❌ 无 | 不会 tool_call |
这两个模型重在"读代码 + 写 patch",没有专门做 tool_call SFT,所以发不出 tool_call JSON。
| 模型 | tool_call 能力 | 备注 |
|---|---|---|
| GPT-4 / GPT-5(OpenAI) | ✅ 极稳定 | 内部有 API 调用日志 + 专门 SFT |
| Claude Sonnet 4.5(Anthropic) | ✅ 极稳定 | 同上 |
| Qwen2.5-Coder-32B-Instruct | ✅ 可用 | Qwen 官方有 function-calling 微调 |
| NousResearch Hermes Function Calling | ✅ 可用 | 专门做 function-calling 训练 |
| Qwen3.8:27b-mlx | ❌ 不可用 | coding 任务微调,无 tool_call SFT |
| Qwen3.6:35b-coding-mxfp8 | ❌ 不可用 | 同上 |
用 opencode 1.14.29 + qwen3.6:35b-coding / qwen3.8:27b-mlx 跑了 5 轮,4 类失败模式:
| 失败模式 | 表现 | 根因 |
|---|---|---|
| "说改不改" | 诊断正确但 edit tool_call 从未发出,"Let me fix" × N 死循环 | 弱模型发不出结构化 JSON tool_call;但 bash/read 文本工具实测 100% 可靠(轮次 ① 60 次调用) |
| "跑偏" | glob/read 游荡到 ~/.claude/skills | 无工作目录约束 |
| "上下文压缩崩溃" | Tool call not allowed while generating summary | 上下文累积 + harness 自动压缩时模型失控 |
—— 即便 L1 + L2 都"通过",L3 模型能力不足仍是稳定失败。
| 来源 | 结论 |
|---|---|
| CodeAct (arXiv:2402.01030) | 可执行动作 vs JSON tool_call:成功率 +20%、轮次 -30%;对未 tool-tuning 的开源模型提升更大 |
| Let Me Speak Freely (arXiv:2408.02442) | 结构化格式限制伤弱模型推理 |
| AgentDiet(本仓 wiki/60) | 上下文不能靠模型自治,须外部强制压缩 |
—— 弱模型走纯文本协议 + harness 侧正则解析,比强行走 JSON tool_call 更可靠。
第四讲 · 任务形态分析:SWE-bench 本质上不需要 tool_call(v1.1 新增)
v1.1 新增:不是模型弱,是任务窄
第三讲解释了"为什么弱模型发不出 tool_call JSON"——但还有更上游的问题:SWE-bench(SWE = Software Engineering 软件工程 + bench = benchmark 基准测试,用真实 GitHub issue 评测代码 agent 的软件工程基准)这个任务本身就不需要 tool_call。"读代码 + 写 patch"这个任务完全可以被完全文本化,harness 把"该读什么"准备好,模型只管吐 diff。三个 ollama runner 不传 tools 字段,不是"因为模型弱",是"因为这个任务根本不需要 tool_call"。弱模型用纯文本 bash 协议反而是顺水推舟。
| 任务形态 | 工具集 | 流程 | 需要什么 |
|---|---|---|---|
| 通用 Agent(能调任何工具) | 运行时才确定(搜天气 / 查日历 / 发邮件 ...) | 动态 | tool_call 是必需(统一接口) |
| 窄义任务(SWE-bench 风格) | 固定(读文件 / 跑测试 / 写文件) | 固定(issue → 找文件 → 改 → 验证) | 工具可预先写死在 harness |
SWE-bench 任务是后者——工具集是确定的,流程也是确定的。tool_call 提供的"通用对话能力"对它冗余。
| 步骤 | 需要什么 | 是否需要"调工具"? |
|---|---|---|
| 1. 读 issue | 文本输入即可 | ❌ |
| 2. 找相关文件 | 已知文件名 + 路径(jsonl 里有) | ❌ 不需要 ls/grep |
| 3. 读文件内容 | 文本输入即可(harness 提前塞 prompt) | ❌ |
| 4. 改代码 | 输出 unified diff | ❌ 不需要 edit 工具 |
| 5. 跑测试验证 | 由 harness 执行 | ❌ 不需要 exec 工具 |
—— 5 步里 0 步真的需要模型调工具。
输入:issue 文本(用户报的问题描述) 输出:unified diff(对哪些文件加什么行 / 删什么行) —— 两端都是纯文本。
模型需要的"信息"(哪些文件、文件里有什么)在任务开始前就已经确定了:
- SWE-bench jsonl 里有 instance_id / repo / base_commit / problem_statement
- 仓库文件已经在 worktree 里(harness 备仓的)
- 关键文件可以用 BM25 / git log 反向定位(ollama-pipeline 路线)
所以"读代码"这件事harness 提前做了,把代码文本塞进 prompt;"写 patch"模型直接吐 unified diff。
prompt: "这是 issue...这是相关文件 a.py 内容...这是相关文件 b.py 内容..." output: "diff --git a/a.py ... @@ -10,3 +10,4 @@ ... +fixed"
—— 这个纯文本 in / 纯文本 out的范式下,tool_call 真的没有位置。
| runner | 怎么绕开 |
|---|---|
| ollama-one-shot | harness 把 issue 塞 prompt,模型一次性吐 patch——连"读文件"都被 harness 跳过(碰运气) |
| ollama-pipeline | harness 先用 BM25 + ast 裁剪代码,把"该读什么"自动准备,模型只看 harness 给的代码包吐 patch |
| ollama-shell | 模型自己写 bash 命令(cat / sed / pytest),harness 执行并回填——用纯文本 bash 协议代替 tool_call 的"调工具"能力 |
—— 三种路线都没用 tool_call 字段,但都完成了"读代码 + 写 patch + 验证"的全部工作。
| 步骤 | 需要什么 | tool_call |
|---|---|---|
| 1. 用户问"今天北京天气怎样" | 输入 | ❌ |
| 2. 模型需要查天气 API | 动态外部数据 | ✅ get_weather |
| 3. 模型需要查日历 | 动态外部数据 | ✅ get_calendar |
| 4. 模型需要发邮件 | 触发外部动作 | ✅ send_email |
| 5. 总结回复用户 | 输出 | ❌ |
—— 通用 Agent 必须 tool_call,因为"做什么工具"在运行时才确定。
"读代码 + 写 patch"这个任务本身就完全可以用纯文本表达——harness 把"该读什么"准备好,模型只管吐 diff。这是任务形态决定的,不是模型能力决定的。tool_call 的真正价值在"通用 Agent 调任意工具"——SWE-bench 任务太窄,工具集 + 流程都固定,harness 可以替模型干"调工具"这件事,所以 tool_call 是冗余的。
—— 三个 ollama runner 不传 tools 字段,不是因为模型弱,是因为这个任务根本不需要 tool_call。弱模型用纯文本 bash 协议反而是顺水推舟。
第五讲 · 等效实现:三个 runner 怎么绕开
范式转移:从 API 层工具调用 → Harness 层工具调用
三个 runner 都把"工具调用"这件事从API 层降级到Harness 层:tool_call 从"模型对 API 的结构化输出"变成"模型对 harness 的纯文本输出",harness 自己解析并执行。协议兼容性让位给模型可执行性——这个权衡是 ollama runner 设计的核心。
强模型稳定输出"] N5["Agent loop 解析 → 调工具 → 回填 tool result"] N1 --> N2 --> N3 --> N4 --> N5 --> N1 end subgraph Shell["🟧 ollama-shell 弱模型 + 纯文本协议"] direction TB S1["Agent loop"] S2["POST messages
不传 tools"] S3["LLM API"] S4["bash 代码块
cat f.py
弱模型 100% 可靠"] S5["Agent loop 正则解析 → 执行 bash → 截断回填"] S1 --> S2 --> S3 --> S4 --> S5 --> S1 end
| OpenAI tool_call 概念 | ollama-shell 等效实现 |
|---|---|
| tools: [{name:"read_file", ...}] | 不传 tools;prompt 明确"输出 bash 块" |
| tool_choice: "auto" | 不传;prompt 限定"每轮一个 bash 块或 SUBMIT" |
| message.tool_calls[].function.name: "read_file" | cat sympy/core/_print_helpers.py |
| message.tool_calls[].function.arguments: {"path": "..."} | bash 命令参数(harness 按 shell token 拆) |
| message.tool_calls[].id(配对键) | shell-transcript.jsonl 里的 step 编号 |
| role:"tool" + tool_call_id 回填 | bash stdout/stderr 作为下一轮 user 消息 |
| finish_reason: "tool_calls" | 下一轮 client.chat_messages() 调用 |
| finish_reason: "stop" | 用户输出 SUBMIT |
结论:能力等价,协议不同。tool_call 的所有语义都在 ollama-shell 中得到了纯文本版复刻。
| runner | 等效路线 | 能力范围 | 代价 |
|---|---|---|---|
| ollama-one-shot | 直接出 patch(不读代码) | 1 个候选 patch | 模型看不到代码,碰运气 |
| ollama-pipeline | BM25 + ast 裁剪代码包 + 采样 N=6 + 验证投票 | 0 tool_call;6 个候选 patch 选最优 | 5-8 min/题;多 LLM 调用 |
| ollama-shell | 纯文本 bash 协议 + 5 道护栏 + 三级超时 | 循环到 SUBMIT;多 tool_call 等效 | 25-40 min/题(50 步 × 30s) |
| tool_call 语义 | one-shot | pipeline | shell |
|---|---|---|---|
| 读文件 | ❌ | ✅(crop_spans) | ✅(cat/read) |
| 写文件 | ❌ | ✅(候选 patch 落盘) | ✅(cat > f <<EOF) |
| 跑测试 | ❌ | ✅(judge_candidates 跑 venv pytest) | ✅(pytest) |
| 多轮规划 | ❌ | ❌(6 个独立采样) | ✅(循环到 SUBMIT) |
| 采样放大 | ❌ | ✅(默认 6 个) | ❌ |
| cwd 约束 | ❌ | ❌ | ✅(CwdJail 护栏) |
| 重复检测 | ❌ | ❌ | ✅(RepeatDetector) |
| max_steps 硬顶 | ❌ | ❌ | ✅(OLLAMA_SHELL_MAX_STEPS=50) |
| 上下文预算 | ❌ | ❌ | ✅(BudgetGuard 外部截断) |
| LeakGuard(git 历史泄漏) | ❌ | ❌ | ✅ |
你在一个代码仓库里修复 issue。每轮你必须且只能输出以下两种之一: 1. 一个 bash 命令块(我会执行并把输出回给你): ```bash <一条命令> ``` 2. 单词 SUBMIT(确认修复完成,我会提取 git diff 作为答案) 红线: - 所有操作限定在当前仓库目录内,禁止访问其它路径 - 禁止修改 tests/ 下任何文件 - 禁止运行 git log / git show / git blame(仓库历史与未来提交对你不可见) - 用预装 venv 的 python 跑测试:<VENV_PYTHON> - 最小改动修复;改动后必须重新运行相关测试验证
—— 明确让模型走"bash 块"路线(这模式模型在预训练时见过海量 bash 教程),避开"tool_call JSON"路线(需要专门 SFT)。
第六讲 · 选型决策:什么时候改回标准 tool_call 协议
选型决策树:4 路径收敛到 4 种 runner
最后一张卡给可执行的决策:什么场景用什么 runner、什么场景必须切到云端。决策只看一个轴:模型本身能不能稳定输出 tool_call JSON——能就上标准协议,不能就走 ollama-shell 等效路线。
gpt-5 / claude-sonnet-4-5
deepseek-reasoner} Start --> Q2{本地大参数 70B+
Qwen2.5-Coder
DeepSeek-Coder-V2} Start --> Q3{本地 27B/35B
coding 模型
qwen3.8:27b-mlx 等} Q1 -->|是| Path1["用 tools 字段
标准 OpenAI tool_call 循环
协议层 + 模型层都支持"] Q2 -->|可能| Path2["可能能用 tools 字段
先小规模验证
注意 Ollama 兼容层方言陷阱"] Q3 -->|是| Path3["不传 tools 字段
用 ollama-shell 纯文本 bash 协议
等效实现 tool_call 全部语义"] Path1 --> Rec1["推荐 kimi-fast 或 qwen-one-shot
或新增 openai-agent runner"] Path2 --> Rec2["先跑 10 题验证
再决定走哪条"] Path3 --> Rec3["推荐 ollama-shell 多轮规划
ollama-pipeline 采样放大
ollama-one-shot 粗筛"]
| 场景 | 行动 |
|---|---|
| 1000+ 题粗筛 | ollama-one-shot(快) |
| 10-100 题稳健 | ollama-pipeline(采样放大) |
| 5-20 题高质量 | ollama-shell(多轮规划) |
| 必须用 tool_call 协议 | 换云端 API(gpt-5 / claude-sonnet-4-5) |
| 必须在本地用 tool_call | 换模型:qwen2.5:7b-instruct 或 nous-hermes:function-calling |
如果从 ollama 切到云端 API,只需替换 _client.py 的 base_url 和认证头,三个 ollama runner 的循环逻辑可以直接复用(或改用 kimi-fast / qwen-one-shot runner 走 one-shot 路径)。
但云端 API 的 tool_call 行为更稳定,有可能反而是 ollama-shell 的循环模式显得冗余(强模型不需要 5 道护栏)。届时可考虑直接用 qwen-one-shot(云端 one-shot + 强模型)或新增 openai-agent runner 走标准 tool_call 协议。
任何"为什么 XX 不能用标准协议"的问题,都可以套这个分层去看:L1(标准在不在)+ L2(兼容端点行为一不一致)+ L3(模型训练数据支不支持)+ 任务形态(这个任务本身需要这个协议吗)。
—— 这四层都"通过"才真能用;任一层薄弱都崩。
收口:把 6 张卡片装回一个心智
- 卡片 1:报告回答 3 个问题;L1/L2/L3 心智模型——分析工程问题的标准分层
- 卡片 2:标准 tool_call 的 4 字段 + 完整往返;tool_call JSON 是"格式技能",需要专门 SFT
- 卡片 3:L1 协议标准 + L2 兼容层方言陷阱 + L3 模型训练数据限制 = 三层叠加;4 类失败模式实测 + 3 篇文献支持
- 卡片 4(v1.1 新增):任务形态分析——SWE-bench 这个任务本身就不需要 tool_call,"读代码 + 写 patch" 5 步全文本化
- 卡片 5:范式转移(API 层 → Harness 层)+ 概念对照表 + 能力矩阵 + ollama-shell prompt 全文
- 卡片 6:选型决策树(按模型能力分支)+ 4 场景行动项 + 切云端注意点
这个 case study 的最大价值不是结论本身,而是"遇到工程问题时分四层去看"的方法论:L1(标准在不在)+ L2(兼容层行为一不一致)+ L3(模型训练数据支不支持)+ 任务形态(这个任务本身需要这个协议吗)。任何"为什么 XX 不能用标准协议"的问题,套这个框架去分析,都能得到稳健的工程结论。