首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >#WorkBuddy# 哪些活儿不必烧积分:把本地 Ollama 模型接进 WorkBuddy 的混合用法(附体检脚本)

#WorkBuddy# 哪些活儿不必烧积分:把本地 Ollama 模型接进 WorkBuddy 的混合用法(附体检脚本)

原创
作者头像
用户12406184
发布于 2026-10-20 13:24:57
发布于 2026-10-20 13:24:57
1620
举报

一、你会遇到的场景

一天里真正在烧积分的,往往不是那些"大活儿",而是一堆碎活儿:把十几个文件按统一规则重命名、给一段长文写八十字的摘要、从一段会议记录里抠出待办事项、把一堆表格字段统一成同一种叫法。

这些活的共同点是:不需要联网、不需要最新知识、输出格式固定、做完就能验收。但它们走的是和"帮我写一份行业分析报告"同一条计费通道。用旗舰模型干格式整理的活儿,就像叫主任医师来贴创可贴——能干,但贵。

二、结论先行

可以下放给本地模型的活儿,判断标准只有三条:输出短、格式固定、不联网。

三条全中 → 下放本地,积分消耗为零。缺任何一条 → 老实走云端,别为了省积分把活儿干砸。这不是玄学,可以用脚本量化验证,下面第四节给可复制的体检脚本。

三、完整操作步骤(macOS,Apple Silicon 实测)

步骤 1:装 Ollama,确认服务在跑

代码语言:bash
复制
# 安装(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) 就说明服务起来了。

步骤 2:验证 OpenAI 兼容接口

WorkBuddy 对接 Ollama 走的是它内置的 OpenAI 兼容接口,所以基址必须以 /v1 结尾,不是裸的 11434。这一步先自测,能省掉后面一半的排查时间。

代码语言:bash
复制
curl -s http://127.0.0.1:11434/v1/models

正常会返回模型列表 JSON。返回空或者连不上,先查 ollama serve 有没有起。

步骤 3:在 WorkBuddy 里接入

路径:设置 → 模型 → 添加自定义模型 → 本地部署(Ollama)。

按官方文档说明,Ollama 启动后本地监听 11434 端口,并自动提供兼容 OpenAI 协议的接口供 WorkBuddy 对接,适用场景明确包含"零成本试用:不用消耗 Token、不用付费 API Key"。填这四项:

字段

填什么

说明

API Base URL

http://localhost:11434/v1

必须是 /v1 结尾

API Key

ollama

本地服务不校验,填非空即可

Model ID

gemma4:e4b

用 ollama list 查到的名字,别自己编

Context Length

按模型能力填

填大了会直接 OOM

保存后,对话界面的模型选择器里会多出一个"自定义模型"分组,切过去就能用。

步骤 4:先体检,再下放(核心)

不要凭感觉决定哪些活儿下放。 用下面这个脚本跑一遍,它会拿三类典型任务各跑三轮,输出耗时中位数和格式合规率,直接给结论。

脚本无第三方依赖,Python 3.9+ 直接跑:

代码语言:python
复制
#!/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()

步骤 5:建立分流规则

把体检结果固化成一张表,贴在提示词模板里,以后不用重复判断:

任务类型

走本地

理由

批量重命名 / 格式统一

✅

输出短、规则固定、零创造

长文摘要(≤80 字)

✅

压缩型任务,本地模型够用

待办提取(结构化 JSON)

✅

但输出项多时耗时会上升

联网检索 / 查最新资料

❌

本地模型没有实时信息

多步工具调用 / 复杂编排

❌

本地模型工具调用能力明显弱于云端

精确数值计算 / 专业判断

❌

出错代价远高于省下的积分

步骤 6:让模型常驻

本地模型有加载开销,跑完就卸载的话每次都在付冷启动的钱。让它常驻:

代码语言:bash
复制
# 后台常驻(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 及以下量化版。

边界——这些情况别用本地模型:

  • 任务需要联网检索或依赖最新信息(本地模型知识有截止)
  • 需要强推理、需要跨域综合判断
  • 涉及精确数值、专业术语密集的领域(输出错误代价太高)
  • 需要多轮工具调用与自主编排的 Agent 任务
  • 需要把对话内容发给模型方的场景——反过来,如果内容敏感、不能出本机,那恰恰是必须用本地模型的理由

六、小结

省积分最有效的办法不是挑便宜的云端模型,而是把一部分活儿干脆挪出计费通道。判断标准就三条:输出短、格式固定、不联网。拿不准就跑一遍体检脚本,两分钟出结论,比拍脑袋靠谱。


原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、你会遇到的场景
  • 二、结论先行
  • 三、完整操作步骤(macOS,Apple Silicon 实测)
    • 步骤 1:装 Ollama,确认服务在跑
    • 步骤 2:验证 OpenAI 兼容接口
    • 步骤 3:在 WorkBuddy 里接入
    • 步骤 4:先体检,再下放(核心)
    • 步骤 5:建立分流规则
    • 步骤 6:让模型常驻
  • 四、实测数据
  • 五、踩坑与边界
  • 六、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档