ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

本地部署MiniCPM5-2B构建收入归因Agent的ReAct实战

本地部署MiniCPM5-2B构建收入归因Agent的ReAct实战 先说结论如果你把 Agent 定义成“接到指令、调用工具、按固定格式回报”的小助手本地部署的 MiniCPM5-2B 完全能扛但如果你指望它像 GPT-4 那样自己拆解复杂问题、多轮自主思考那它会还你一堆漂亮的废话和编造的数字。最近我用手头一台普通 GPU 机器跑通了这件事在本地部署 MiniCPM5-2B写了一个“收入归因 Agent”用来回答“这周收入为什么涨了”“哪个渠道贡献最大”“广告 ROI 掉到多少了”这类日常业务问题。这篇文章不吹不黑把我踩过的坑、设计思路、完整代码、测试记录都摊开讲清楚。如果你也想在低成本设备上搞一个本地 Agent可以直接照着抄作业。1. 为什么要在本地跑一个收入归因Agent1.1 业务痛点周报归因为什么累先交代一下背景。我手上有一个线上工具类产品收入来源比较杂自然搜索、付费广告、社交媒体内容、老客复购每个渠道都有各自的后台数据。以前每周一早上我的固定动作是导出各渠道数据拼成一张总表算环比再人工判断到底是哪个渠道拉动了增长。运气好20分钟能出结论运气不好发现广告后台和订单库的口径对不上一上午就没了。更麻烦的是归因逻辑。收入涨了 10%到底是自然搜索涨了还是某个渠道的投放加码了还是整体市场大盘在涨人工分析的时候大部分人就是看哪个渠道环比涨得多直接说“因为付费广告涨了”这个结论非常容易被挑战。所以我想做一个“收入归因 Agent”把这些脏活累活自动化查数据、算增量、算贡献率、最后直接生成一段可以放进周报的分析文字。1.2 选型思路2B模型加Ollama还是云端大模型一开始我自然想用云端大模型 API毕竟现在市面上中文推理能力强的 API 一大把。但有几道坎过不去第一客户的订单数据、渠道成本数据都属于敏感业务数据我这边没有权限把它们传到第三方接口第二这玩意儿不是用一次就完长期跑每周一轮分析token 成本虽然不夸张但也是持续开销第三公司内网环境有时不允许访问公网 API本地模型是唯一选项。那为什么不选 7B 或者 13B 的模型我手上的 GPU 是一张 12GB 显存的卡跑 7B 量化模型也能跑但推理速度会明显慢下来而且显存吃掉一大半机器干不了别的。2B 级别的模型显存占用小速度够快尤其是部署工具链已经很成熟Ollama 一行命令就能拉下来跑起来。另一个考虑是收入归因这个任务本质上不是开放闲聊它的流程是固定的——拿数据、算增量、归因到渠道。模型需要做的更多是“按规矩办事”而不是“天马行空想结论”。这种场景对模型智商的要求没那么高但对指令遵循能力有要求。我打算用 MiniCPM5-2B 做一次压力测试看看它到底能扛多远。1.3 设备与软硬件清单列一下我这边跑通的全套环境方便你对照GPUNVIDIA RTX 3060 12GBCPUi5-12400F内存32GB系统Windows 11 WSL2Ubuntu 22.04推理工具Ollama最新稳定版模型MiniCPM5-2BQ4_K_M 量化版本编程语言Python 3.10数据源SQLite 数据库表里存了最近 60 天的分渠道日收入数据这里有个小建议如果你没有独立显卡32GB 内存的机器也能用 Ollama 跑 CPU 推理2B 模型在 CPU 上虽然不快但也不是不能用。我后期测试过纯 CPU 推理生成速度大约每秒 8-12 个 token对单条问答来说可以接受批量分析就会比较熬人。2. 环境部署与模型起步2.1 Ollama 安装与模型拉取Ollama 是现在本地跑大模型最省事的工具没有之一。安装过程不复杂Linux 上一条命令搞定curl -fsSL https://ollama.com/install.sh | shWindows 用户去官网下安装包装完直接有命令行工具。装好后拉模型ollama pull minicpm5-2b拉下来之后先手动跑一下确认模型能正常对话ollama run minicpm5-2b输入“你好用一句话介绍你自己”看它能不能正常输出中文。这一步很重要能快速排除模型损坏或者环境变量的问题。我遇到过的情况是 Windows 下 Ollama 默认监听 localhost:11434但在 WSL2 里访问会偶发连接超时解决方案是检查 Windows 防火墙是否放行了 11434 端口或者干脆在 WSL2 里重新装一份 Ollama。2.2 量化版本怎么选Ollama 拉下来的模型默认是官方选好的量化版本。如果你像我喜欢折腾可以手动指定量化级别。我对比过几个版本的差异量化级别模型大小显存占用推理速度效果Q4_K_M约1.6GB约4GB快日常够用Q8_0约2.4GB约5GB中略好但提升有限F16约4.5GB约7GB慢理论最好实测下来我的感受是2B 模型的瓶颈根本不在量化精度上而是在推理能力和指令遵循上。Q8_0 比 Q4_K_M 在跑分上也许好看一点但在“听懂人话、按格式输出、调用工具”这些真实任务上的差异很小。所以我最后固定使用 Q4_K_M这也是官方默认值省显存还快。2.3 简单连通性测试确认模型能跑之后用 Python 调一次 Ollama 的 HTTP 接口验证 OpenAPI 风格的 chat 接口可用import requests response requests.post( http://localhost:11434/api/chat, json{ model: minicpm5-2b, messages: [{role: user, content: 11等于几}], stream: False, options: {temperature: 0.2} } ) print(response.json()[message][content])返回正常说明环境就绪了。这里我提前把 temperature 调到 0.2对 Agent 场景来说低温减少随机性比默认的 0.8 可靠很多。这个参数贯穿了整个项目几乎所有翻车问题都能靠降温度缓解。3. Agent架构与代码实现3.1 整体流程设计收入归因 Agent 的完整流程我一开始想得比较复杂意图识别、多轮对话管理、动态工具编排、记忆模块。后来跑了几次发现2B 模型根本撑不住那么重的设计。它的工作记忆很浅稍微多几个步骤就开始遗忘前面说过什么。所以我最终收敛成一条极简流水线用户问题进来模型输出一个 JSONJSON 里指定要调用哪个工具和参数代码去执行工具把结果拼回对话里模型再基于结果生成最终回答。最多循环 3 步超过就终止。这个设计叫 ReAct 模式的微缩版只保留了“推理-行动-观察”的骨架去掉了复杂的状态管理。这样设计有个好处不管模型多笨只要你给它设定好工具列表和输出格式它出错的概率会大幅下降。因为大段计算和查数都交给了代码模型只做两件事决定调哪个工具、把工具结果组织成人话。3.2 工具函数设计工具层是整个 Agent 的灵魂。我把所有需要用到的能力都封装成了带类型标注的 Python 函数。核心是这三个第一个是get_sales_data负责从 SQLite 里查分渠道的营销数据。这个函数返回的是一个字典列表每个渠道一行包含本周收入、上周收入、本周成本等字段。第二个是run_attribution负责做归因计算吃掉第一个函数的输出算出来每个渠道的增量、增长率、对总盘子的贡献率。第三个是search_history_report负责在历史分析文档里搜相似结论方便模型参考之前的写法。数据表结构我用的是一个很简单的销售日表CREATE TABLE sales_daily ( date TEXT, channel TEXT, revenue REAL, cost REAL );原始数据长这样(2025-06-01, paid_ads, 32000.0, 15000.0)其中paid_ads是付费广告organic是自然搜索social是社媒内容repeat是老客复购。3.3 ReAct主循环与JSON协议先上主循环代码这是整个 Agent 的核心。完整代码我放在下面可以直接保存为agent.py运行import json import re import sqlite3 import requests OLLAMA_URL http://localhost:11434/api/chat MODEL_NAME minicpm5-2b MAX_STEPS 3 SYSTEM_PROMPT 你是收入归因分析助手只做一件事帮业务同学解答收入变化的原因。 你必须遵守以下铁律 1. 必须先调用工具获取数据禁止直接回答任何数字。 2. 所有数字必须一字不差地来自工具返回结果禁止计算、估算、编造。 3. 每次只输出一个 JSON格式 {thought: 你这一步的思考, action: 工具名, action_input: {参数: 值}} 4. 如果已经拿到足够的工具结果输出 {thought: 我已经拿到数据, action: final, action_input: {analysis: 最终回答}} 可用工具 - get_sales_data: 查询分渠道收入数据参数 group_by 可选 channel/date默认 channel - run_attribution: 对收入数据进行归因计算输入是 get_sales_data 的结果 - search_history_report: 搜索历史分析报告参数 keyword 必填 def get_sales_data(group_bychannel): conn sqlite3.connect(sales.db) if group_by channel: rows conn.execute( SELECT channel, SUM(CASE WHEN date date(now, -7 day) THEN revenue END) AS this_week, SUM(CASE WHEN date date(now, -14 day) AND date date(now, -7 day) THEN revenue END) AS last_week FROM sales_daily GROUP BY channel ).fetchall() conn.close() return [{channel: r[0], this_week: r[1] or 0, last_week: r[2] or 0} for r in rows] # 简化版按日期分组同理 conn.close() return [] def run_attribution(channel_data): if not channel_data: return {error: 无数据} this_total sum(c[this_week] for c in channel_data) last_total sum(c[last_week] for c in channel_data) growth this_total - last_total channels [] for c in channel_data: delta c[this_week] - c[last_week] channels.append({ channel: c[channel], delta: round(delta, 2), growth_rate: round(delta / c[last_week] * 100, 2) if c[last_week] else None, contribution: round(delta / growth * 100, 2) if growth else 0 }) return {total_growth: round(growth, 2), channels: channels} def search_history_report(keyword): # 模拟返回历史报告摘要 return [{title: 6月第1周周报, summary: 付费广告增量贡献率最高达53%}] TOOLS { get_sales_data: get_sales_data, run_attribution: run_attribution, search_history_report: search_history_report, } def call_model(messages): response requests.post(OLLAMA_URL, json{ model: MODEL_NAME, messages: messages, stream: False, format: json, options: {temperature: 0.2} }) return response.json()[message][content] def extract_json(text): match re.search(r\{.*\}, text, re.S) if not match: raise ValueError(No JSON found) return json.loads(match.group()) def run_agent(question): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: question} ] for step in range(MAX_STEPS): text call_model(messages) try: act extract_json(text) except Exception: messages.append({role: assistant, content: text}) messages.append({role: user, content: 输出必须是合法JSON请重新输出。}) continue action act.get(action) if action final: return act[action_input][analysis] if action in TOOLS: try: tool_result TOOLS[action](**act.get(action_input, {})) messages.append({role: assistant, content: text}) messages.append({role: tool, content: json.dumps(tool_result, ensure_asciiFalse)}) except Exception as e: messages.append({role: assistant, content: text}) messages.append({role: user, content: f工具执行出错{e}请换一个工具或参数。}) else: messages.append({role: assistant, content: text}) messages.append({role: user, content: f未知工具{action}只能使用 , .join(TOOLS.keys())}) return 达到最大步骤数请简化问题。 if __name__ __main__: print(run_agent(最近一周哪个渠道收入最高))这里有两个细节值得注意。第一format: json是 Ollama 对 JSON 输出的支持能极大减少模型把 JSON 和自然语言混在一起的情况但这个字段只在部分模型上效果好MiniCPM5-2B 实测是支持的。第二temperature: 0.2是反复试出来的最佳值再低并不会更好再高就开始乱编工具名。3.4 提示词工程给2B模型立规矩很多人跑小模型 Agent 失败问题不在模型不行而是提示词写得太“宽容”。2B 模型和 70B 模型的提示词策略完全不一样。大模型你给它一个模糊目标它能自己脑补出合理路径小模型你给的目标越模糊它就越放飞自我。我的系统提示词策略是“立规矩”三件套先说身份和应用范围再列铁律最后给输出格式的 JSON schema。最关键的是那条“所有数字必须一字不差地来自工具返回结果”。没有这条铁律时模型经常自己脑补出“上周收入 130 万”而真实数据可能是 128.9 万。有了这条虽然它偶尔还是会犯但至少我可以在代码层拦截。提示词里我还加了一条“每次只输出一个 JSON”这也很重要。2B 模型喜欢先输出一段“好的我来分析一下”然后才是 JSON。用 JSON mode 之后这个问题缓解了很多但偶尔还会犯所以主循环里用正则提取{...}做兜底。4. 实测2B模型的能力边界4.1 测试集设计为了搞清楚模型到底能扛多远我准备了五类测试问题单步查询类“最近一周哪个渠道收入最高”简单对比类“自然搜索和付费广告哪个收入高”归因判断类“为什么这周总盘子涨了”综合建议类“付费广告 ROI 下降了问题出在哪”多轮追问类先问“哪个渠道增长最快”再追问“这个渠道为什么增长快”。
返回列表