ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大语言模型实战——最小化模型评测:用 ollama + qwen 跑通 F1 值验证

大语言模型实战——最小化模型评测:用 ollama + qwen 跑通 F1 值验证 1. 为什么我要在本地搭一套最小评测闭环大语言模型评测这件事听起来像是大厂才需要做的重活但实际落地时你会发现真正卡住普通开发者的不是算力而是「不知道从哪一步开始」。我最初想验证一个本地 qwen 模型在中文问答上的表现翻了一圈资料要么是直接甩一个榜单分数要么是讲一堆指标公式却不给能跑的脚本。于是我自己动手搭了一套最小闭环用 ollama 拉起 qwen围绕 F1 值设计一个小评测集写一个能复用的评测脚本最后跑一次完整流程看结果。这套流程适合谁适合手上有本地模型、想快速验证效果、又不想引入复杂评测框架的人。它不追求刷榜追求的是「今天下午就能跑通」。核心检索词就三个大语言模型、模型评测、F1 值。F1 值在这里的作用是把精确率和召回率合成一个数让你一眼看出模型答得准不准、全不全。ollama 负责把模型跑起来qwen 是待测对象评测集和脚本是你要自己掌控的部分。我试过用现成的评测平台配置项多到让人分心最后还是回到「一个脚本 一个配置文件」的路子。下面按顺序讲先准备环境和模型再设计评测集和 config.toml然后写脚本骨架接着跑一次完整验证最后把常见报错列出来。每一步都给可复制的命令和代码你跟着改路径就能用。2. TaoToken 前置把模型调用和评测解耦在本地跑评测最容易被忽略的是「模型调用层」和「评测逻辑层」的耦合。如果你把 ollama 的调用直接写死在评测脚本里换一个模型或换一个接口就要改一堆代码。我的做法是加一层轻量的 API 网关把模型对话统一成 OpenAI 兼容格式这样评测脚本只认接口不认底层是 ollama 还是别的。TaoToken 在这里的角色就是这层网关。它的 API 地址是 https://taotoken.net/api兼容常见的 chat completions 协议。你可以在本地 ollama 和评测脚本之间插一个配置项让脚本既能直连 ollama也能走统一接口。这样做的好处是评测逻辑不变模型来源可切换。如果你只是本地验证ollama 直连就够了但如果你想把评测结果和线上模型对比或者团队里有人用不同模型统一接口会省很多事。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。这些链接先记着后面配置里会用到。注意评测脚本里不要把密钥硬编码进代码用环境变量或单独的 .env 文件提交代码前检查一遍。3. 可复制配置ollama 拉起 qwen 与 config.toml3.1 安装 ollama 并拉取 qwen先确认本机已经装好 ollama。Linux 和 macOS 用官方脚本Windows 用安装包。装完后验证版本ollama --version拉取 qwen 模型。这里选 qwen2.5:7b体积和效果比较平衡如果你的机器内存小可以换 qwen2.5:3bollama pull qwen2.5:7b拉完后确认模型在列表里ollama list启动服务默认监听 11434 端口ollama serve另开一个终端测试模型能不能正常对话curl http://localhost:11434/api/chat -d { model: qwen2.5:7b, messages: [{role: user, content: 用一句话解释什么是F1值}], stream: false }如果返回里有 message.content说明模型通了。这一步很关键很多人后面评测报错其实是模型根本没拉下来或服务没起。3.2 设计最小评测集评测集不用大20 到 50 条就够验证流程。格式用 jsonl一行一条字段包括 input、context、answers。answers 用列表方便以后扩展多参考答案。示例{input: 《孔子世家谱》的编修工作历时多久, context: 《孔子世家谱》第五次大修后裔资料收集工作历时10年完成。, answers: [10年。]} {input: 鹤壁市经济社会发展面临哪些困难, context: 鹤壁市面临经济下行压力加大、转型任务艰巨、风险隐患不容忽视等困难。, answers: [经济下行压力加大、转型任务艰巨、风险隐患不容忽视。]}把文件存成eval_set.jsonl。注意 context 不要过长本地模型上下文有限建议单条控制在 2000 字以内。如果你的原始数据 context 很长可以在脚本里做截断后面会讲。3.3 config.toml 配置片段用一个 config.toml 把模型、路径、评测参数集中管理避免脚本里到处是魔法数字[model] provider ollama base_url http://localhost:11434 model_name qwen2.5:7b max_tokens 256 temperature 0.0 [eval] dataset_path ./eval_set.jsonl output_path ./predictions.jsonl max_context_length 2000 prompt_template 阅读以下文字并用中文简短回答\n\n{context}\n\n现在请基于上面的文章回答下面的问题只告诉我答案不要输出任何其他字词。\n\n问题{input}\n回答 [scoring] use_jieba true remove_punctuation truetemperature 设 0.0 是为了让评测结果可复现。max_tokens 控制生成长度太长会拖慢评测。prompt_template 里的占位符要和脚本里的 format 字段对应。如果你要走统一接口而不是本地 ollama把 provider 改成openai_compatiblebase_url 改成https://taotoken.net/api再补一个 api_key 字段从环境变量读。这样同一套评测脚本可以切换模型来源。4. 评测脚本骨架从预测到 F1 计算4.1 加载配置和数据集脚本用 Python依赖 requests、jieba、tomliPython 3.11 以下需要装 tomli 读 toml。先写加载部分import json import os import requests import jieba from collections import Counter import tomli def load_config(path./config.toml): with open(path, rb) as f: return tomli.load(f) def load_jsonl(path): data [] with open(path, r, encodingutf-8) as f: for line in f: line line.strip() if line: data.append(json.loads(line)) return data4.2 调用模型生成预测核心函数是 chat兼容 ollama 和 OpenAI 兼容接口def chat(config, prompt): model_cfg config[model] if model_cfg[provider] ollama: url f{model_cfg[base_url]}/api/chat payload { model: model_cfg[model_name], messages: [{role: user, content: prompt}], stream: False, options: {temperature: model_cfg[temperature]} } resp requests.post(url, jsonpayload, timeout120) resp.raise_for_status() return resp.json()[message][content].strip() else: url f{model_cfg[base_url]}/v1/chat/completions headers {Authorization: fBearer {os.environ.get(TAOTOKEN_API_KEY, )}} payload { model: model_cfg[model_name], messages: [{role: user, content: prompt}], temperature: model_cfg[temperature], max_tokens: model_cfg[max_tokens] } resp requests.post(url, jsonpayload, headersheaders, timeout120) resp.raise_for_status() return resp.json()[choices][0][message][content].strip()生成预测的主循环注意 context 截断逻辑def generate_predictions(config, dataset): template config[eval][prompt_template] max_len config[eval][max_context_length] out_path config[eval][output_path] results [] for item in dataset: context item.get(context, ) if len(context) max_len: half max_len // 2 context context[:half] context[-half:] prompt template.format(contextcontext, inputitem[input]) pred chat(config, prompt) record {pred: pred, answers: item[answers]} results.append(record) with open(out_path, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) return results4.3 数据清洗与 F1 计算中文评测必须做标点清洗否则标点会拉低分数。清洗函数import string def normalize_zh_answer(s): def white_space_fix(text): return .join(text.split()) def remove_punc(text): cn_punc 。、〃》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟〰〾〿–—‘’‛“”„‟…‧﹏. all_punc set(string.punctuation cn_punc) return .join(ch for ch in text if ch not in all_punc) return white_space_fix(remove_punc(s.lower()))F1 计算函数def f1_score(prediction, ground_truth): pred_tokens list(jieba.cut(normalize_zh_answer(prediction))) gt_tokens list(jieba.cut(normalize_zh_answer(ground_truth))) common Counter(pred_tokens) Counter(gt_tokens) num_same sum(common.values()) if num_same 0: return 0.0 precision num_same / len(pred_tokens) recall num_same / len(gt_tokens) return (2 * precision * recall) / (precision recall)整体评测取每条最高分再平均def evaluate(results): total 0.0 for r in results: pred r[pred] answers r[answers] best max(f1_score(pred, gt) for gt in answers) total best return round(100 * total / len(results), 2)把上面拼起来主入口if __name__ __main__: cfg load_config() ds load_jsonl(cfg[eval][dataset_path]) preds generate_predictions(cfg, ds) score evaluate(preds) print(fF1 score: {score})5. 验证请求与成功结果先跑一条数据验证链路通不通。把评测集只留一条执行python eval.py如果看到类似输出F1 score: 62.5说明预测和打分都跑通了。再跑完整评测集观察 predictions.jsonl 里的内容{pred: 历时10年。, answers: [10年。]} {pred: 经济下行压力加大、转型任务艰巨、风险隐患不容忽视。, answers: [经济下行压力加大、转型任务艰巨、风险隐患不容忽视。]}第一条 pred 和 answers 分词后交集是「10」「年」F1 会比较高第二条几乎完全匹配F1 接近 1。整体分数在 40 到 70 之间都算正常取决于评测集难度和模型大小。如果你用的是 3b 模型分数低一些不用慌重点是流程跑通。想验证模型对话本身是否正常可以单独发一条请求curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:qwen2.5:7b,messages:[{role:user,content:你好}]}返回正常说明接口层没问题。模型对话入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite可以对照返回格式检查字段。6. 本篇常见错排查6.1 ollama 连接被拒绝报错Connection refused或Max retries exceeded先确认 ollama serve 是否在跑curl http://localhost:11434/api/tags如果这条也失败说明服务没起。检查端口是否被占用或者 ollama 是否装在了别的用户下。Windows 上有时需要手动启动 ollama 应用。6.2 模型名写错导致 404ollama 返回model not found用ollama list看实际模型名。注意 qwen2.5:7b 和 qwen:7b 是两个不同的 tagconfig.toml 里要和 list 输出完全一致。6.3 F1 分数异常低如果分数低于 10先检查分词和清洗。常见原因是 pred 里带了大量解释性文字而 answers 很短交集自然少。可以在 prompt 里强调「只告诉我答案不要输出任何其他字词」或者在后处理里截取第一句。另一个原因是 context 截断把关键信息切掉了适当调大 max_context_length。6.4 jieba 分词结果不符合预期jieba 对专有名词的分词可能和你的预期不同。如果评测集里有很多领域术语可以加载自定义词典jieba.load_userdict(user_dict.txt)每行一个词格式为「词语 词频 词性」词频和词性可省略。6.5 统一接口返回 401走 TaoToken 接口时如果报 401检查环境变量 TAOTOKEN_API_KEY 是否设置以及请求头格式是否为Bearer key。API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite确认 key 没有过期或被禁用。接入细节看 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。6.6 评测跑得太慢本地 7b 模型单条生成可能要几秒到十几秒50 条就是几分钟。可以先把 max_tokens 调小或者用 3b 模型先验证流程。如果要做长期编码或 Agent 场景的评测考虑用 Coding Plan 把调用层独立出来入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。7. 把评测闭环用起来这套最小闭环跑通后你可以做几件事换模型对比同一评测集的 F1 分数调整 prompt 模板看分数变化扩充评测集覆盖更多场景。关键是脚本和配置分离换模型只改 config.toml评测逻辑不动。如果你想把评测接入 CI可以在每次模型更新后自动跑一遍把分数写进日志。如果团队用 Claude Code 做开发也可以把评测脚本挂到 Anthropic 兼容的调用层上入口在 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite。控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite可以看调用记录和用量。最后提醒一句评测集的质量比数量重要。20 条覆盖不同题型的数据比 200 条同质化数据更能暴露问题。F1 值只是起点真正有价值的是你从错误案例里看到的模型短板。
返回列表