一天里真正在烧积分的,往往不是那些"大活儿",而是一堆碎活儿:把十几个文件按统一规则重命名、给一段长文写八十字的摘要、从一段会议记录里抠出待办事项、把一堆表格字段统一成同一种叫法。
这些活的共同点是:不需要联网、不需要最新知识、输出格式固定、做完就能验收。但它们走的是和"帮我写一份行业分析报告"同一条计费通道。用旗舰模型干格式整理的活儿,就像叫主任医师来贴创可贴——能干,但贵。
可以下放给本地模型的活儿,判断标准只有三条:输出短、格式固定、不联网。
三条全中 → 下放本地,积分消耗为零。缺任何一条 → 老实走云端,别为了省积分把活儿干砸。这不是玄学,可以用脚本量化验证,下面第四节给可复制的体检脚本。
# 安装(Homebrew,macOS)
brew install ollama
# 拉一个适合干活的小模型(示例用 gemma4:e4b,约 9.6GB)
ollama pull gemma4:e4b
# 确认服务在监听默认端口
lsof -nP -iTCP:11434 -sTCP:LISTEN看到 TCP 127.0.0.1:11434 (LISTEN) 就说明服务起来了。
WorkBuddy 对接 Ollama 走的是它内置的 OpenAI 兼容接口,所以基址必须以 /v1 结尾,不是裸的 11434。这一步先自测,能省掉后面一半的排查时间。
curl -s http://127.0.0.1:11434/v1/models正常会返回模型列表 JSON。返回空或者连不上,先查 ollama serve 有没有起。
路径:设置 → 模型 → 添加自定义模型 → 本地部署(Ollama)。
按官方文档说明,Ollama 启动后本地监听 11434 端口,并自动提供兼容 OpenAI 协议的接口供 WorkBuddy 对接,适用场景明确包含"零成本试用:不用消耗 Token、不用付费 API Key"。填这四项:
字段 | 填什么 | 说明 |
|---|---|---|
API Base URL |
| 必须是 |
API Key |
| 本地服务不校验,填非空即可 |
Model ID |
| 用 |
Context Length | 按模型能力填 | 填大了会直接 OOM |
保存后,对话界面的模型选择器里会多出一个"自定义模型"分组,切过去就能用。
不要凭感觉决定哪些活儿下放。 用下面这个脚本跑一遍,它会拿三类典型任务各跑三轮,输出耗时中位数和格式合规率,直接给结论。
脚本无第三方依赖,Python 3.9+ 直接跑:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
local_model_probe.py —— 本地模型「能不能接这活儿」体检脚本
运行环境:macOS(Apple Silicon 实测),Python 3.9+,无第三方依赖
用法:
python3 local_model_probe.py # 用内置样例跑 3 轮
python3 local_model_probe.py --rounds 5
python3 local_model_probe.py --model qwen3:8b
"""
import argparse, json, statistics, time, urllib.error, urllib.request
OLLAMA_BASE = "http://127.0.0.1:11434/v1"
DEFAULT_MODEL = "gemma4:e4b"
# 体检用样例任务:刻意选「结构化、低创造、高频」三类活儿
# 说明:样例内容全部为虚构演示数据,不含任何真实业务信息
TASK_BANK = {
"会议纪要待办提取": {
"system": ("你是会议纪要助手。只输出 JSON 数组,不要任何解释文字。"
"每项包含 todo(动作)、owner(负责人,未知填null)、due(截止时间原文,无则null)。"),
"user": ("从下面这段会议记录里提取待办事项:\n"
"张三说样品检测报告下周三前必须给到采购部。"
"李四提了个问题,客户那边的验收标准还没最终确认,需要再跟对方核对一次。"
"王五表示仓库那边的新货架本周就能装完,装完他会拍照发群里。"
"最后大家同意下次例会时间不变。\n只输出 JSON 数组。"),
"check": lambda out: out.strip().startswith("["),
},
"批量重命名规则生成": {
"system": ("你是文件整理助手。用户给一批文件名和命名规则,"
"你只输出重命名后的文件名列表,一行一个,不要序号、不要解释。"),
"user": ("规则:项目缩写_日期_序号.扩展名,日期用 YYYYMMDD,序号三位补零。\n"
"原文件:立项说明.docx、现场照片.jpg、结算单.xlsx、验收记录.pdf\n"
"项目缩写:DEMO,日期:2026年10月17日。\n只输出四个新文件名,一行一个。"),
"check": lambda out: out.count("\n") >= 3 and "DEMO_20261017" in out,
},
"长文本摘要": {
"system": "你是摘要助手。用不超过 80 字的中文概括要点,只输出摘要本身。",
"user": ("请概括下面这段话:\n本周的主要工作集中在三块。第一块是流程梳理,把原来散落在"
"多个表格里的登记环节合并成了一张总表,字段从原来的二十多个精简到九个,填写时间明显下降。"
"第二块是历史数据补录,把过去半年的纸质记录录入系统,期间发现有三处填写口径不一致,已经统一。"
"第三块是对接测试,与上下游系统做了两次联调,第一次因为字段命名不一致失败,第二次通过。"
"下周计划继续推进剩余模块的验证。"),
"check": lambda out: 5 < len(out.strip()) < 200,
},
}
def strip_fence(text: str) -> str:
"""剥掉本地小模型最爱加的 ```json / ``` 围栏。
实测里最常踩的坑:system 里明明写了「只输出 JSON」,
本地模型还是习惯性包一层 Markdown 代码块,下游 json.loads 直接炸。
所以校验和落库之前必须先剥壳。
"""
t = text.strip()
if t.startswith("```"):
t = t.split("\n", 1)[1] if "\n" in t else ""
if t.rstrip().endswith("```"):
t = t.rstrip()[:-3]
return t.strip()
def chat(base, api_key, model, system, user, temperature=0.2, timeout=300):
"""调用 OpenAI 兼容的 /chat/completions,返回 内容+耗时+用量。"""
payload = {
"model": model,
"messages": [{"role": "system", "content": system},
{"role": "user", "content": user}],
"temperature": temperature,
"stream": False,
}
req = urllib.request.Request(
base.rstrip("/") + "/chat/completions",
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json",
"Authorization": "Bearer " + api_key},
method="POST",
)
t0 = time.time()
with urllib.request.urlopen(req, timeout=timeout) as resp:
data = json.loads(resp.read().decode("utf-8"))
elapsed = time.time() - t0
content = data["choices"][0]["message"]["content"]
return {"content": content.strip(),
"content_clean": strip_fence(content),
"elapsed": elapsed,
"usage": data.get("usage", {})}
def probe(base, api_key, model, rounds):
print("=" * 62)
print(f"体检目标:{model} @ {base}")
print(f"每类任务跑 {rounds} 轮,取中位数")
print("=" * 62)
summary = []
for name, task in TASK_BANK.items():
times, oks, sample = [], 0, ""
for i in range(rounds):
try:
r = chat(base, api_key, model, task["system"], task["user"])
except urllib.error.URLError as e:
print(f" [!] {name} 第{i+1}轮请求失败:{e}")
continue
except (KeyError, json.JSONDecodeError) as e:
print(f" [!] {name} 第{i+1}轮返回解析失败:{e}")
continue
times.append(r["elapsed"])
if task["check"](r["content_clean"]):
oks += 1
sample = r["content_clean"]
if not times:
print(f"\n{name}:全部失败,检查服务是否启动")
continue
med = statistics.median(times)
print(f"\n{name}")
print(f" 耗时中位数:{med:.2f} s (min {min(times):.2f} / max {max(times):.2f})")
print(f" 格式合规率:{oks}/{len(times)}")
print(f" 样例输出:{sample[:160].replace(chr(10), ' / ')}")
summary.append((name, med, oks, len(times)))
if summary:
print("\n" + "-" * 62)
print("结论表(可下放 = 格式合规率 100% 且 耗时可接受)")
for name, med, oks, total in summary:
verdict = "可下放" if oks == total else "不建议下放"
print(f" {name:<14} {med:>6.2f}s {oks}/{total} {verdict}")
print("-" * 62)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--base", default=OLLAMA_BASE)
ap.add_argument("--model", default=DEFAULT_MODEL)
ap.add_argument("--api-key", default="ollama")
ap.add_argument("--rounds", type=int, default=3)
args = ap.parse_args()
# 连通性自检,避免后面一堆超时误判
try:
with urllib.request.urlopen(args.base.rstrip("/") + "/models", timeout=5) as resp:
models = [m["id"] for m in json.loads(resp.read())["data"]]
print(f"服务在线,可用模型:{models}")
except Exception as e:
print(f"[致命] 连不上 {args.base}:{e}")
print("排查:ollama serve 起了吗?端口是 11434 吗?")
return
if args.model and args.model not in models:
print(f"[警告] 模型 {args.model} 不在列表里,可能没拉取:ollama pull {args.model}")
probe(args.base, args.api_key, args.model, args.rounds)
if __name__ == "__main__":
main()把体检结果固化成一张表,贴在提示词模板里,以后不用重复判断:
任务类型 | 走本地 | 理由 |
|---|---|---|
批量重命名 / 格式统一 | ✅ | 输出短、规则固定、零创造 |
长文摘要(≤80 字) | ✅ | 压缩型任务,本地模型够用 |
待办提取(结构化 JSON) | ✅ | 但输出项多时耗时会上升 |
联网检索 / 查最新资料 | ❌ | 本地模型没有实时信息 |
多步工具调用 / 复杂编排 | ❌ | 本地模型工具调用能力明显弱于云端 |
精确数值计算 / 专业判断 | ❌ | 出错代价远高于省下的积分 |
本地模型有加载开销,跑完就卸载的话每次都在付冷启动的钱。让它常驻:
# 后台常驻(macOS)
nohup ollama serve > /tmp/ollama.log 2>&1 &
# 或者用 brew services 托管
brew services start ollama以下为本机真实执行结果,环境:MacBook Air M5 / 24GB 统一内存 / macOS / Ollama 0.34.4 / 模型 gemma4:e4b(9.6GB),体检脚本每类任务跑 3 轮取中位数。
任务 | 耗时中位数 | 格式合规率 |
|---|---|---|
批量重命名规则生成 | 2.23 s | 3/3 |
长文本摘要 | 2.15 s | 3/3 |
会议纪要待办提取 | 21.61 s | 3/3 |
吞吐实测:同一条短指令,冷启动首次请求 20.50 s(含模型加载,输出 587 tokens),热态第二次 17.39 s(输出 578 tokens),折合 约 29–33 tokens/s。
结论很直接:本地模型的耗时几乎完全由输出 token 数决定。输出 40 字以内的活儿两秒搞定,输出 600 字就要二十秒。所以"输出短"这条判据不是感觉,是能算出来的。
积分消耗方面,官方文档明确把本地部署归入"零成本试用:不用消耗 Token、不用付费 API Key"。本人账号侧的实际积分变动【待本人实测补充:发任务前后各截一次个人中心积分页做对比】。
坑 1:本地模型爱加代码围栏。 这是本次实测真踩到的。system 里写了"只输出 JSON 数组",模型照样给你包一层 ```json,下游 json.loads 直接抛异常。第一轮体检合规率是 0/3,加上 strip_fence() 剥壳之后才变成 3/3。凡是本地模型输出要给程序消费的,剥壳这一步不能省。
坑 2:别拿输出长度不当回事。 同样一个模型,摘要 2 秒、待办提取 21 秒,差在输出 token 数。批量跑几百条待办提取,本地模型会把你的机器占满且慢得离谱——这类活儿反而该走云端。
坑 3:工具调用能力是分水岭。 本地模型在"照格式输出"上够用,在"自己决定调什么工具、调几次"上明显吃力。涉及多步编排、需要 Agent 自主跑命令的任务,别下放。
坑 4:内存是硬约束。 9.6GB 的模型放在 24GB 统一内存的机器上,会和本机其他应用抢资源。跑大模型时机器偶发卡顿属正常现象;8GB 内存的机器建议只跑 7B 及以下量化版。
边界——这些情况别用本地模型:
省积分最有效的办法不是挑便宜的云端模型,而是把一部分活儿干脆挪出计费通道。判断标准就三条:输出短、格式固定、不联网。拿不准就跑一遍体检脚本,两分钟出结论,比拍脑袋靠谱。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。