
家里那台天猫精灵吃灰多久了我猜很多人买回来用了一个月就只剩定闹钟和问天气这两个功能。我坦白说我家这台也一度走到这个结局直到我开始认真折腾本地大模型部署——突然意识到这音箱硬件其实没毛病缺的只是一颗属于自己的大脑。这一篇是AI人工智能系列第五篇也是“东方仙盟”项目的开篇之作。项目要解决的事很朴素把天猫精灵的云端固定技能替换成我自己部署的本地大模型服务让音箱背后的大模型换成DeepSeek也可以换成Qwen跑通“天猫精灵 - 云端技能平台 - 自建后端服务 - 本地大模型 - 语音回复”这条完整链路。这一期叫“练气期”意思是最基础的入门闭环能转起来先打通任督二脉不搞花活。如果你会一点HTTP接口开发能操作Linux命令行手头有一台带显卡哪怕8G显存的机器这篇可以直接照着抄。整个项目的工程代码量并不大核心就是一个回调服务加一个模型调度难的只是把每个环节的坑都填平。1. 练气期目标拆解一条语音背后要过几道门1.1 官方技能与自建服务之间差的不是硬件天猫精灵本身是一套完整的语音交互硬件麦克风阵列做拾音本地做唤醒云端做ASR识别和NLU理解然后是技能分发。普通用户接触到的“问天气、放歌、控制灯泡”本质都是平台上预先定义好的技能服务。这些技能稳定但对折腾型玩家来说太封闭了——你不能给它自定义system prompt不能把私有知识库塞进去不能让它调用你自己写的工具。那绕开官方技能直接用天猫精灵做一个“大模型语音入口”行不行行而且这是官方支持的合规路径。天猫精灵开放平台提供了自定义技能能力平台负责把用户语音转成文本再通过NLU解析出意图然后把结构化请求POST到你自己的HTTP服务上。后续逻辑完全由你掌控——你是接云端大模型、本地大模型还是自己写的规则引擎平台都不关心。差的只是你要在自己服务器上把模型、服务、证书这些都支棱起来。1.2 一条语音从嘴边到耳朵要过七道门我花了一个下午把整条链路画出来实测下来每一环都有明确分工唤醒和拾音你说“天猫精灵”本地唤醒引擎响应录制后续语音。ASR语音识别音频上传到天猫精灵云端转成文本。NLU意图解析云端平台判断你在唤起哪个技能解析槽位。回调分发平台把结构化请求POST到我在服务器上配置的HTTPS地址。自建服务逻辑解析请求、组装上下文、调用本地大模型。大模型生成Ollama里跑的DeepSeek根据system prompt和对话历史生成回答。NLG与TTS自建服务把回答组装成技能响应格式平台转成语音音箱播报。前三步是天猫精灵云端的活我们管不了但从第四步开始就全是我们自己地盘。练气期项目的地界就是从第四步到第七步这半程。可能有人会问为什么还要过云端不能全部本地吗难度不在自己这端而在天猫精灵的固件和App本身是跟云端绑定的硬件不开放自定义唤醒词和裸输入流。走官方技能通道是投入产出比最高的合规路径也没有侵犯任何条款的问题。1.3 为什么这一期叫练气期修仙小说里练气期就是“能感知灵气、能运行功法但谈不上术法神通”。放到这个项目里练气期就是先把主链路跑通语音进来文字出去模型正常回复。不做知识库检索不做复杂多轮记忆不做多房间联动。目的是把每个环节都验证到位给后面的筑基期打地基。我也把后续阶段提前定了名筑基期加RAG知识库和会话持久化金丹期把Agent工具链铺开让天猫精灵真能控制设备元婴期再考虑多终端协同。这样一次只跨一个境界每一期都有可演示的产物不会陷入“什么都想做、什么都不成”的泥潭。2. 本地大模型这口“丹田灵气”Ollama与DeepSeek的选型与部署2.1 为什么不直接买云端API非要本地部署练气期的初衷是搞懂和掌控。商业API当然更方便但有几个现实问题绕不过去数据隐私家里日常对话我不太想让第三方云服务拿去分析。可定制性API能调模型但Prompt、函数调用、系统提示词的组合链路都锁在厂商的协议里。成本对话密集测试时按token计费其实不便宜本地部署是一次性硬件投入。本地部署这条路上可选方案不少Ollama、vLLM、llama.cpp、LM Studio。排序下来Ollama胜在“最省心”安装一条命令模型一条命令拉取API风格接近OpenAI社区生态成熟。vLLM适合追求吞吐的生产环境但配置和学习成本高筑基期再考虑。llama.cpp适合纯CPU环境或者要极致裁剪的场景。练气期别纠结上Ollama。2.2 安装与拉模型实操我用一台Ubuntu 22.04服务器先更新系统然后执行安装脚本curl -fsSL https://ollama.com/install.sh | sh装完确认版本ollama --version接着拉取模型。练气期推荐DeepSeek-R1的7B量化版中文对话能力和推理表现都在线如果你的显存只有6G可以退到Qwen2.5:3b只是回答深度会明显降一级。ollama pull deepseek-r1:7b拉取完成后跑一个最简单的验证确认模型能正常出字ollama run deepseek-r1:7b 用一句话介绍你自己这里有个新手常踩的坑如果机器内存不足16G模型加载阶段就可能被OOM杀进程。看到的报错五花八门有的说连接被拒有的直接没反应。先free -h看剩余内存低于8G就老老实实换3B模型。另外注意Ollama第一次加载模型会比较慢要把模型完全载入显存后再发请求不然响应时间会吓人。2.3 让Ollama能被自建服务调用模型在本地跑通了还不够自建服务要通过HTTP调它。默认情况下Ollama只监听127.0.0.1Docker容器或远程服务访问不到需要设置环境变量再启动export OLLAMA_HOST0.0.0.0 ollama serve验证API是否可用curl http://localhost:11434/api/chat \ -H Content-Type: application/json \ -d {model:deepseek-r1:7b,messages:[{role:user,content:你好}],stream:false}能拿到带message.content的JSON响应这口丹田灵气就算激活了。注意OLLAMA_HOST0.0.0.0意味着局域网内所有机器都能访问这个API后面一定要用防火墙限制来源只放行自建服务所在的机器或 Docker 网络。2.4 用Docker把Ollama隔离起来为了让后续整个项目干净我建议一开始就把Ollama以容器方式跑数据挂在本机目录sudo docker run -d \ --name ollama \ --gpus all \ -v /data/ollama:/root/.ollama \ -e OLLAMA_HOST0.0.0.0 \ -p 11434:11434 \ ollama/ollama:latest--gpus all是NVIDIA显卡的用法纯CPU机器去掉这个参数但要做好换小模型的心理准备。模型文件放在/data/ollama下面以后重装容器不会丢模型。进容器拉模型sudo docker exec -it ollama ollama pull deepseek-r1:7b我在这里踩过一个很深的坑宿主机直接跑Ollama时模型缓存在~/.ollama容器化之后默认不共享导致容器里找不到模型又硬生生重新下载一遍。把-v挂载目录固定好这类问题一次避开。下面是几个模型在练气期阶段的选型对比硬性条件直接对标模型显存要求内存要求中文表现推理速度(4090)适用场景deepseek-r1:7b6-8G16G优秀20-30 token/s练气期主力qwen2.5:7b6-8G16G优秀25-35 token/s通用对话/工具调用qwen2.5:3b3-4G8G良好40-50 token/s低配机器保底3. 说话要过“仙门外门”天猫精灵自定义技能创建与回调联调3.1 在天猫精灵开放平台建技能要让自己写的服务被天猫精灵唤起需要在天猫精灵开放平台创建一个“自定义技能”。登录开发者控制台后创建技能时选择“自定义技能”填两个关键信息技能名称和调用名称。调用名称就是用户语音唤起你说的话我填的是“东方仙盟”。填好后用户对音箱说“天猫精灵打开东方仙盟”天猫精灵就会把后续语音解析到我们这个技能下面。命名尽量用口语化、短词太长用户记不住太生僻语音识别率会下降。不同时期的平台界面可能改版但“自定义技能”这个概念一直保留字段找类似的填就行。这步不涉及代码但有两个前置条件要注意开发者账号要提前实名认证技能涉及隐私政策时也要如实填写不然后台上传服务地址会被卡住。3.2 意图设计练气期只做一个“聊天意图”技能的核心是意图配置。天猫精灵云端会做NLU解析把用户的话映射到某个意图上再把解析结果POST给你的服务。练气期不要把意图设计得太复杂一个就够了叫“chat”或者“闲聊”只做一件事——把用户原始文本完整透传给你的后端。配置意图时提供几个典型用户表达作为语料比如“你好”“讲个笑话”“你知道什么是修仙吗”。平台会拿这些语料训练NLU模型但实际用户说的话千奇百怪所以一定要留兜底逻辑如果意图识别为None或识别错误也把整句话透传下去。这样哪怕意图没匹配上你的大模型也能接住不会让用户听到生硬的“暂不支持”。我在配置时还加了几个槽位比如城市、时间用于后面扩展工具调用。练气期不真正解析槽位但先定义好字段后面接天气、时间工具时就不用再改技能结构这一步省下来的时间在联调阶段特别值。3.3 回调协议的最小实现技能配置的最后两步填写后端服务地址上传签名公钥。服务地址必须是公网HTTPS自建服务收到POST后需要校验请求签名防止别人伪造请求。从平台到自建服务的请求体大致是这种结构字段以你账号下平台文档为准我按主链路简化{ token: 平台生成的身份令牌, userId: openid_xxx, query: { intentName: chat, utterance: 你好 }, session: { sessionId: 6f6a6d2e-8f3b-4ddb-9c1b-2e7a1b3c4d5e, attributes: {} } }自建服务需要返回的结果结构大致是这样{ returnCode: 0, result: { toSay: 道友你好我是东方仙盟练气期的守山弟子。, nlg: { type: text, value: 道友你好我是东方仙盟练气期的守山弟子。 } } }returnCode为0表示成功result.toSay是播报文本nlg里放结构化展示内容。练气期只要保证toSay和nlg.value一致音箱就能正常播报。如果想在App端展示卡片nlg里还有别的类型字段但那是后话了。3.4 联调阶段绕不过的三个坎第一个坎是HTTPS。平台要求公网可访问的HTTPS服务。我的做法是Nginx反向代理服务器上申请Lets Encrypt证书自动续期443端口转发到Spring Boot的8080。不要用自签名证书去试平台不会认。第二个坎是签名校验。平台允许你上传一个签名公钥回调请求头里带签名服务端要用公钥验签。这步不能省一旦服务地址泄露任何人都可以直接POST你的接口白嫖你的GPU算力。第三个坎是响应超时。天猫精灵对技能服务的响应时间卡得很死从云端发起回调到你返回结果通常只有几秒窗口。本地7B模型在GPU上生成一句话一般1到3秒勉强够用但要是让模型思考太久或者网络抖动平台就会向用户播报“技能开了小差”。练气期的兜底方案很粗暴设置2.5秒的模型请求超时超时就返回一句“道友稍等我正在凝聚灵气请再问一次”。真正的异步长任务处理留到筑基期再做。4. 把“丹田”和“外门”接起来核心服务端开发4.1 技术栈选择Java还是Python为什么是Spring Boot回调服务就是普通HTTP Web服务选型空间很大。我的纠结在Java Spring Boot和Python FastAPI之间。FastAPI的优势是写起来飞快、异步原生、Python生态跟AI贴合紧密。Spring Boot的优势是工程化成熟适合对接平台回调这类URL到URL的简单调用后面如果想用LangChain4j做AgentJava生态也有完整支持。考虑到筑基期、金丹期要在这个服务上叠加工具链和稳定运维我最终选了Java 17 Spring Boot 3.x。这不是说FastAPI不行如果你更熟Python完全可以用它HTTP接口逻辑是一样的。还有一个隐形理由Java服务在处理“请求-响应”这种转发逻辑时心智负担很低。回调进来的字段、出去的字段用DTO定义清楚后面自己复用也清晰。4.2 核心接口代码接收回调、转发模型、组织返回我建了一个Spring Boot工程核心就一个Controller代码很短但每行都有讲究RestController RequestMapping(/genie) public class GenieCallbackController { private final OllamaChatService ollamaChatService; public GenieCallbackController(OllamaChatService ollamaChatService) { this.ollamaChatService ollamaChatService; } PostMapping(/skill/chat) public MapString, Object handle(RequestBody GenieRequest req) { String utterance req.getQuery().getUtterance(); String sessionId req.getSession().getSessionId(); String reply; try { reply ollamaChatService.chat(sessionId, utterance); } catch (Exception e) { reply 道友稍等我正在凝聚灵气请再问一次。; } MapString, Object nlg new HashMap(); nlg.put(type, text); nlg.put(value, reply); MapString, Object result new HashMap(); result.put(toSay, reply); result.put(nlg, nlg); MapString, Object response new HashMap(); response.put(returnCode, 0); response.put(result, result); return response; } }关键点在于整个方法被try-catch包住任何异常都不能让平台收到5xx。天猫精灵端用户只会听到“技能开了小差”那是体验灾难。宁可返回兜底文案也要保证returnCode恒为0。4.3 调用Ollama消息历史与超时设置OllamaChatService的核心逻辑是调Ollama的/api/chat接口。练气期我把会话历史放在内存里用sessionId做key最多保留最近6条消息防止上下文太长拖慢推理Service public class OllamaChatService { private final RestTemplate restTemplate new RestTemplate(); private final MapString, DequeChatMessage sessions new ConcurrentHashMap(); private static final String OLLAMA_URL http://127.0.0.1:11434/api/chat; public String chat(String sessionId, String userText) { DequeChatMessage history sessions.computeIfAbsent(sessionId, k - new ArrayDeque()); history.addLast(new ChatMessage(user, userText)); while (history.size() 6) { history.removeFirst(); } MapString, Object reqBody new HashMap(); reqBody.put(model, deepseek-r1:7b); reqBody.put(stream, false); reqBody.put(messages, new ArrayList(history)); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntityMapString, Object entity new HttpEntity(reqBody, headers); ResponseEntityMap resp restTemplate.postForEntity(OLLAMA_URL, entity, Map.class); Map data resp.getBody(); String reply (String) ((Map) data.get(message)).get(content); history.addLast(new ChatMessage(assistant, reply)); return reply.trim(); } }内存存储意味着服务重启后会话全丢练气期可以接受因为目的是验证链路。但有两个细节要注意ConcurrentHashMap只保证基本线程安全ArrayDeque在并发场景其实不严格安全后面真要扛并发时要换成Redis。这里不展开知道边界在哪就行。超时设置很重要。我给RestTemplate配了连接超时和读取超时SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(2000); factory.setReadTimeout(3000); RestTemplate restTemplate new RestTemplate(factory);读取超时3秒配合前面的兜底文案基本能保证平台端的响应窗口不炸。实际调优中我用4090跑deepseek-r1:7b生成一句20字以内的回答大约1.5秒但如果用户连环追问复杂问题token数上去之后很容易超3秒。模型超时时间宁可保守也不能让平台先报错。4.4 服务配置与工程杂项服务本身的配置不复杂但有一个容易被忽略的点回调服务一定要把端口固定好配置到Nginx转发里。application.yml中server: port: 8080另外平台回调的Content-Type是application/jsonSpring Boot默认能处理。如果遇到“Unsupported Media Type”这类错误先检查Controller是不是写了错误映射。Java版本建议17以上Spring Boot 3.x不再支持JDK 8新建工程时就要盯住。如果你的服务端技术栈是Python逻辑完全一样FastAPI只要在Post接口里做同样的字段解析和模型调用即可唯一的差异在于异步写法。练气期怎么顺手怎么来。5. 从“会说话”到“会做事”给练气期弟子装上工具5.1 为什么练气期就要提Agent天猫精灵如果只会闲聊那跟一个会说话的摆件有什么区别真正有价值的是让它能执行动作、查询数据。这就是Agent要解决的问题大模型本身不感知外部世界但它可以通过工具调用Function Calling来决定调用什么函数、传什么参数然后把工具返回结果组织成用户能听懂的话。练气期不需要把Agent框架完整铺开但我会让链路从一开始就预留工具位。这样到筑基期接RAG、接智能家居都不用推翻重来。5.2 Function Calling的最小链路大模型Agent的基础模式是在system prompt里描述有哪些工具每个工具的参数格式是什么。用户提问后模型输出的不一定是最终回答而是一个结构化的“函数调用请求”。服务端解析这个请求执行真正的工具代码。把工具执行结果回填给模型模型再生成面向用户的最终回答。在Ollama里模型是否原生支持function calling取决于模型本身。DeepSeek-R1在指令跟随和工具调用上的表现尚可Qwen2.5系列对工具调用的支持则更稳定一些。练气期可以先用Qwen2.5:7b来做Agent验证。给一个简单的system prompt片段你是东方仙盟的练气期守山弟子说话要带一点修仙者气质但要简洁。 当你需要查询天气时输出 {tool:query_weather,arguments:{city:杭州}} 如果不需要工具直接正常回答。服务端拿到模型输出后先判断字符串里是否包含tool结构包含就提取JSON、执行工具、把结果拼进对话上下文再问一次模型。这套逻辑很土但原理跟正式Function Calling一致适合练气期吃透机制。5.3 一个可以直接抄的“查天气”工具我写了一个最简工具不依赖任何第三方SDK用高德或和风天气的免费接口都能实现。核心是暴露一个可被调用的Java方法Component public class WeatherTool { private final RestTemplate restTemplate new RestTemplate(); public String queryWeather(String city) { String url https://example-weather-api.com/now?city URLEncoder.encode(city, StandardCharsets.UTF_8) key System.getenv(WEATHER_API_KEY); Map resp restTemplate.getForObject(url, Map.class); return {用户问题: city 天气, 结果: resp.get(text) }; } }方法返回的字符串会被拼回上下文让模型最终说出“道友杭州现在多云气温26度适合御剑飞行。”重点是工具返回值要尽量结构化别把无关文本塞进去否则模型容易被带偏。5.4 MCP和Dify在练气期的定位MCPModel Context Protocol是最近热度很高的模型上下文协议简单理解就是给Agent工具调用制定了一套统一标准。Dify这类平台也已经支持MCP工具接入可以把工具注册到可视化工作流里。练气期我没有直接上MCP因为引入它等于多一层抽象和调试负担。但我在服务里预留了MCP客户端依赖等筑基期工具多了再按标准协议把现有工具包装成MCP服务是水到渠成的事。对只想跑通整体流程的读者我的建议也是先自己写两三个工具方法理解调度循环再上MCP。反过来学容易在抽象层迷路。6. 上线部署与踩坑实录6.1 Docker Compose编排整条服务链练气期的部署目标一条docker compose up命令把Ollama、自建服务、Nginx全部拉起来。我写了这样的编排文件services: ollama: image: ollama/ollama:latest container_name: ollama restart: always volumes: - ./data/ollama:/root/.ollama environment: - OLLAMA_HOST0.0.0.0 ports: - 127.0.0.1:11434:11434 deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] skill-server: build: ./skill-server container_name: skill-server restart: always depends_on: - ollama environment: - OLLAMA_URLhttp://ollama:11434 - SERVER_PORT8080 ports: - 8080:8080 nginx: image: nginx:1.27-alpine container_name: genie-nginx restart: always depends_on: - skill-server volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/ssl:/etc/nginx/ssl:ro ports: - 80:80 - 443:443一个细节Ollama端口我映射到127.0.0.1:11434而不是0.0.0.0。这样宿主机和Docker网络都能访问但不会把Ollama直接暴露到公网。自建服务容器内通过http://ollama:11434访问走的是Docker内部网络。安全上宁可多绕一道也不要让模型接口裸奔。6.2 部署过程中最容易翻车的三个点第一个是Docker的GPU支持。NVIDIA机器要在宿主机装好nvidia-container-toolkit否则compose里写再漂亮的device配置也起不来。验证方式是docker exec进容器跑nvidia-smi确认能看到显卡。第二个是Nginx证书路径和权限。Lets Encrypt证书文件权限不对Nginx容器会拒绝启动。我习惯把证书目录以只读方式挂载文件属主设为rootNginx master进程能读就行。第三个是自建服务里OLLAMA_URL的坑。本地调试时我用http://127.0.0.1:11434容器化之后必须改成http://ollama:11434否则容器里访问的是自身永远连不上模型。这个问题我修了半小时最后是docker exec进容器里curl排查出来的。遇到容器网络问题先进容器敲命令别在宿主机对着配置文件猜。6.3 延迟实测与调优手段我整理了一次完整请求的耗时分布单位毫秒。阶段耗时说明ASR语音识别300-600天猫精灵云端NLU意图解析100-300天猫精灵云端回调到自建服务50-150公网HTTPS本地模型推理1200-2500deepseek-r1:7b取决于token数平台TTS播报400-800天猫精灵云端总耗时大约2.1到4.3秒。用户体感是说话后有一两秒停顿属于可接受范围但还有挤压空间。我做了三个优化把模型换成量化版本q4_K_M比默认版本推理更快显存占用更小。在system prompt里要求“回答不超过50字”token数少了推理时间直接降三分之一。把Nginx的gzip打开回调请求和响应都做压缩公网传输那几十毫秒也能省。这些优化做完多数请求能压到3秒以内语音交互的紧张感少了很多。不过要注意别为了压时长把回答质量砍得太狠用户听太敷衍的AI一样会腻。6.4 避免被薅算力签名校验与限流服务一旦公网可访问就要面对被扫描和刷接口的风险。我做三层防护平台签名校验只接受带合法签名的回调请求。IP白名单如果平台方提供固定回调IP段在Nginx的allow配置里加上非白名单直接拒绝。简单限流用Spring Boot的拦截器按userId做每分钟调用次数限制超过就返回兜底文案防止某个账号死循环把模型拖垮。这层防护在练气期做到这个程度足够也不会花太多时间。真正的高并发防护到了金丹期再折腾。7. 练气期之后筑基期再战这一篇写到这里链路已经完整天猫精灵自定义技能、本地Ollama模型、Spring Boot回调节点、Agent工具雏形、Docker Compose部署全部串起来了。我自己的体感是经过这一轮再看到“某设备接入大模型”的项目不会再觉得高深本质都是这条主链路的不同变形。筑基期我准备做三件事一是把会话历史从内存迁到Redis支持更长上下文和服务重启不丢记忆二是加一个RAG知识库把手头资料变成可检索的仙术典籍让模型回答有凭有据三是把MCP工具标准化把天气、定时任务、智能家居设备都接进来让天猫精灵真正变成一个能执行命令的门派弟子。练气期这段路我踩过最大的坑其实不是技术问题而是“什么都想做”。一开始我打算同时接知识库、做多轮记忆、调音色结果链路都没跑通就陷进细节里。后来老老实实拆成最小闭环反而一个周末就跑完了。如果你也在做类似的事建议先从“能用”开始让一句话能从你的音箱进到你的模型再从模型里出来变成语音这就是巨大的胜利。剩下的境界一层一层破。