1. 为什么 ollama run 之后总觉得“少点东西”
你大概率遇到过这个场景:本地用 Ollama 拉了个 qwen2.5 或者 llama3,ollama run进去聊得挺顺,但一旦让它按固定格式输出、按固定步骤分析数据、或者调用某个外部脚本,它就开始自由发挥。云端平台那种“导入一个 Skill 就能稳定干活”的体验,在本地好像凭空消失了。
先说清楚一件事:Ollama 本身没有插件市场,也没有ollama install skill这种命令。它的职责非常窄——下载和管理 GGUF 模型文件、把模型加载进内存或显存、暴露一个本地 HTTP 推理接口(默认127.0.0.1:11434)、通过 Modelfile 设置系统提示词和采样参数。它不扫描技能目录,不识别用户意图,也不会自动路由到某个工具。
但“本地模型不能有 skill”是错的。Skill 的本质是一套标准化的业务流程、规则和输出模板,它不需要嵌进模型权重里。真正缺的是那一层调度骨架:谁来决定这次该用哪个 skill、谁来把 skill 文本和用户问题拼成完整 prompt、谁来接收模型返回并做后处理。这篇文章就围绕 Modelfile 和外部调度程序,搭一套可复用的骨架,让本地 Ollama 模型也能稳定挂载 skill 能力。适合已经在本地跑模型、想往工具调用和自动化工作流方向走的开发者。
2. 前置准备:Ollama 服务与 TaoToken 通道
在写 Modelfile 之前,先把两个基础件确认好。
第一个是 Ollama 服务本身。安装完成后确认版本和服务状态:
ollama --version ollama list curl http://127.0.0.1:11434/api/tags如果curl能返回模型列表的 JSON,说明本地推理服务已经就绪。后面所有调度程序都通过这个端口和模型通信,不需要改动 Ollama 本体。
第二个是统一 Key/API 通道。本地模型负责推理,但调度程序里往往还要接其他 AI 工具——比如做意图路由时调一个更强的模型判断该选哪个 skill,或者把本地结果交给云端模型做二次整理。这时候如果每个工具都单独配一套 Key,维护成本会很高。我习惯用 TaoToken 把 Key 和 API 通道统一起来,官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。它的作用是让你在调度程序里用同一套凭证访问不同模型,本地 Ollama 和外部模型可以共存于同一条调度链路里。
需要先拿到 API Key 的话,去控制台创建:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。接入文档可以参考 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面写了不同语言下的调用方式。如果你后面要做长期编码或 Agent 类任务,可以了解下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
注意:TaoToken 在这里的角色是统一通道,不是替代 Ollama。本地推理仍然走
127.0.0.1:11434,只有需要外部模型参与调度时才走 TaoToken 的 API。
3. 可复制的 Modelfile 骨架与 skill 注册配置
这一节给两套东西:一套是 Modelfile 骨架,用来把单个 skill 固化进模型;另一套是外部调度程序里的 skill 注册配置,用来做多技能动态加载。两者可以组合使用。
3.1 Modelfile 骨架:把 skill 提示词打包成新模型
Modelfile 的语法很简单,核心就几个指令:FROM指定基础模型,SYSTEM写系统提示词,PARAMETER调采样参数。下面是一个可直接复制的骨架,把 skill 内容放在SYSTEM三引号里:
FROM qwen2.5:7b PARAMETER temperature 0.2 PARAMETER top_p 0.9 PARAMETER num_ctx 8192 SYSTEM """ 【Skill 名称】渠道留存分析 【触发条件】用户提供包含 dt、channel、register_cnt、retain_7d 字段的明细数据 【执行步骤】 1. 校验数据:检查空值、负数,异常行标注但不剔除 2. 计算 7 日留存率 = retain_7d / register_cnt,保留两位小数 3. 按渠道聚合总新增与平均留存率 4. 检测留存率环比下降超过 10% 的记录,标记为异常波动 5. 只基于计算结果描述现象,不推测外部原因 【输出格式】 一、数据概况 二、渠道指标汇总表 三、异常波动清单 四、客观结论 【禁止行为】 禁止编造输入中不存在的数据;禁止输出模板以外的自由段落。 """打包和运行:
ollama create retention-skill -f ./Modelfile ollama run retention-skill这套骨架的优点是零依赖,命令行直接可用。缺点是 skill 永久占用系统提示词的 token,技能一多就会互相干扰,而且每次改 skill 都要重新ollama create。所以它只适合单一固定场景。
3.2 skill 注册配置:外部调度程序动态加载
多技能场景要把调度层拆出来。做法是在本地建一个 skills 目录,每个 skill 一个 Markdown 文件,再用一个注册表描述触发关键词和文件路径。目录结构如下:
skills/ registry.json retention.md sql_review.md report_format.mdregistry.json是注册配置,把 skill 名称、触发词、文件路径对应起来:
{ "skills": [ { "name": "retention", "keywords": ["留存", "渠道", "7日"], "path": "./skills/retention.md", "priority": 10 }, { "name": "sql_review", "keywords": ["SQL", "慢查询", "索引"], "path": "./skills/sql_review.md", "priority": 8 } ] }调度程序读取这个注册表,根据用户输入命中关键词,加载对应 skill 文本,再拼成完整 prompt 发给 Ollama。这样新增技能只需要加一个 md 文件和在 registry 里加一条记录,不用重新打包模型。
3.3 调度程序核心逻辑
下面是一段可直接跑的 Python 调度骨架,包含 skill 加载、关键词路由和 Ollama 调用:
import json import requests OLLAMA_URL = "http://127.0.0.1:11434/api/generate" MODEL = "qwen2.5:7b" def load_registry(path="./skills/registry.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) def load_skill(path): with open(path, "r", encoding="utf-8") as f: return f.read() def route_skill(query, registry): hits = [] for item in registry["skills"]: for kw in item["keywords"]: if kw in query: hits.append(item) break if not hits: return None hits.sort(key=lambda x: x["priority"], reverse=True) return hits[0] def build_prompt(skill_text, query): return f"{skill_text}\n\n用户请求:{query}" def call_ollama(prompt): resp = requests.post(OLLAMA_URL, json={ "model": MODEL, "prompt": prompt, "stream": False, "options": {"temperature": 0.2} }, timeout=120) resp.raise_for_status() return resp.json()["response"] if __name__ == "__main__": registry = load_registry() query = "帮我分析这份渠道留存数据,字段有 dt、channel、register_cnt、retain_7d" skill = route_skill(query, registry) if skill: skill_text = load_skill(skill["path"]) prompt = build_prompt(skill_text, query) else: prompt = query print(call_ollama(prompt))这段代码里,Ollama 只负责推理,skill 的选择和拼接全在调度层完成。如果你想让路由更聪明,可以把关键词匹配换成向量检索,或者调一次外部模型做意图分类——这时候就用 TaoToken 的统一通道,在call_ollama旁边加一个走https://taotoken.net/api的分类函数即可,Key 从环境变量读取,不硬编码。
4. 端到端验证:一次完整的 skill 调用
配置写完了,得验证它真的能跑通。准备一份测试数据,存成test_data.txt:
dt,channel,register_cnt,retain_7d 2024-06-01,A,1000,320 2024-06-01,B,800,200 2024-06-02,A,1100,300 2024-06-02,B,850,150 2024-06-03,A,1050,290 2024-06-03,B,900,120先验证 Modelfile 打包的模型:
ollama run retention-skill < test_data.txt预期结果是模型按SYSTEM里定义的四个部分输出,包含数据概况、渠道汇总表、异常波动清单和结论,且不会出现“可能因为市场投放”这类无依据推测。
再验证外部调度程序:
python scheduler.py如果路由命中retention,程序会加载skills/retention.md并拼接 prompt。实测下来,返回结果的结构稳定性和 Modelfile 版本接近,但好处是你可以随时改retention.md而不用重新打包模型。验证成功的标志有三个:输出包含模板要求的四个段落、异常波动清单里能正确标出 B 渠道 6 月 3 日留存率下降超过 10%、结论部分没有编造数据。
如果你还想验证模型对话能力本身,可以走 TaoToken 的模型对话入口做对比:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,把同样的 prompt 发给云端模型,观察本地和云端在格式遵循上的差异,方便调 skill 文本。
5. 本篇常见错排查
5.1 ollama create 报错 “no Modelfile found”
原因通常是文件名大小写或路径不对。Modelfile 的 M 必须大写,且要在命令里显式指定路径:
ls -l ./Modelfile ollama create retention-skill -f ./Modelfile如果文件在别的目录,-f后面写相对或绝对路径都行,但不要只写Modelfile而当前目录没有。
5.2 调度程序连不上 11434
先确认 Ollama 服务在跑:
curl -s http://127.0.0.1:11434/api/tags | head如果连接被拒绝,说明服务没启动。Linux 下用systemctl status ollama看状态,macOS 下确认菜单栏图标是否在。另外注意,如果调度程序跑在容器里,127.0.0.1指向的是容器自身,要换成宿主机的可达地址。
5.3 skill 命中错误或完全不命中
关键词路由的典型问题是触发词太宽或太窄。比如“留存”这个词可能同时出现在多个 skill 的触发词里,导致优先级高的总是被选中。排查方法是打印路由结果:
print("命中 skill:", skill["name"] if skill else "无")如果发现总是命中同一个,检查registry.json里各 skill 的priority和keywords是否有重叠。更稳的做法是给每个 skill 加一个exclude_keywords字段,命中排除词时直接跳过。
5.4 模型不按模板输出
先确认 skill 文本里的输出格式是否足够具体。模糊的“输出报告”不如“按以下四段输出,每段标题固定”有效。其次检查temperature,高于 0.5 时格式遵循会明显下降,建议设到 0.2 以下。如果还是跑偏,把 skill 文本里的禁止行为写得更硬,比如“禁止输出模板以外的任何段落”。
5.5 外部模型调用返回 401
如果调度程序里接了 TaoToken 做意图分类,401 通常是 Key 没读到或环境变量名写错。确认:
echo $TAOTOKEN_API_KEY然后在代码里用os.environ.get("TAOTOKEN_API_KEY")读取,不要写死在源码里。请求地址用https://taotoken.net/api,不要带多余路径。
6. 把调度层沉淀成可复用资产
走到这里,你应该已经有一套能跑的骨架了:Modelfile 负责单技能固化,registry + 调度程序负责多技能动态加载,Ollama 始终只做推理。这套结构最大的好处是 skill 和模型解耦——换基础模型只需要改FROM那一行,加技能只需要往skills/目录扔文件。
如果你后面要把这套东西接到更长的编码或 Agent 工作流里,建议把调度程序里的模型调用统一走 TaoToken 通道,Key 和 API 地址集中管理,本地和云端模型用同一套调用方式。接入细节看文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 创建。长期跑编码任务的话,Coding Plan 的额度模型比按次调用更划算:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。
最后留一个实用技巧:skill 文本不要一次写太长,把公共约束抽成一个base.md,每个 skill 文件开头用一行@include base.md引用,调度程序加载时做一次文本替换。这样改公共规则只需要动一个文件,所有 skill 同步生效。