最近一直在收拾一个自己攒了很久的项目:把本机大模型真正塞进一台“学伴/生活机器人”里,而且技术栈从头到尾只用 C#。上位机是 Visual Studio 里的 ASP.NET MVC,模型推理跑在 Windows 本机,直接加载 .gguf 和 .onnx 两种格式;下位机是用 .NET NanoFramework 写的嵌入式端。折腾完一圈回头总结,整个工程的思路其实非常清晰,想写出来给打算在 .NET 生态里做本地 AI 或机器人项目的朋友参考。
这个东西能做什么?简单说,一台普通 Windows 电脑就是“大脑”,装一个量化后的开源大模型,MVC 站点提供对话界面和 HTTP 接口;机器人本体负责交互——说话、读传感器、做动作。电脑和机器人通过串口通信,全程离线,数据不出本机,没有云 API 的费用和请求延迟。如果你手上有 .NET 开发经验,想碰本地大模型却不知道从哪下手;或者想给嵌入式小设备接上“AI 大脑”,这篇文章应该能帮你少走不少弯路。下面按我实际的推进顺序来讲:先定架构,再搭推理底座,然后包成 MVC 服务,最后联调下位机。
1. 先想清楚:本机 LLM 怎么进机器人
1.1 为什么非要本机跑,而不是连云 API
很多人第一个问题是:机器人对话直接调云端的模型 API 不就行了,为什么要费劲在本地跑?这个项目最初的需求场景决定了走向——学伴机器人主要给学生在离线环境使用,比如教室里、家里断网的时候,或者是数据敏感的场景,不要求对话内容经过任何外部服务器。云 API 的按量计费看着便宜,一旦真做成 24 小时待机的设备,每天几千次请求,成本马上变得不可控。
本机 LLM 的好处是确定性的成本和确定性的隐私。模型文件下载下来放硬盘里,以后每次推理只耗电,不耗钱。数据全程留在本机,不碰网络,也没有“聊天记录被厂商拿去训练”的担忧。缺点也很明显:本机算力有限,模型不能太大;推理速度比云端旗舰模型慢一个数量级。所以这个方案适合“小模型、专用场景、并发量低”的机器人,而不是拿来和 GPT 级别的大模型比文采。
从工程角度看,本机推理还带来一个额外优势:整个系统可以完全离线自洽。上位机、Web 界面、下位机串口,全部在局域网和个人电脑上闭环,调试时不依赖外网,部署时也只要拷贝一个文件夹。做嵌入式项目的人应该都懂,少一个外部依赖就是少一类故障。
1.2 三层架构拆解:PC 大脑、MVC 服务、NanoFramework 下位机
整个项目的硬件拓扑其实很简单,就是一台 Windows 主机加一个单片机控制板。软件上我把它拆成三层,每层的边界非常清晰:
| 层级 | 技术选型 | 职责 | 运行位置 |
|---|---|---|---|
| 交互层 | ASP.NET MVC + Razor/JS | Web 管理界面、对话页面、API 接口 | Windows 本机 |
| 模型层 | LLamaSharp (.gguf) / ONNX Runtime (.onnx) | 加载模型、执行推理、维护会话上下文 | Windows 本机 |
| 下位机层 | .NET NanoFramework + GPIO/UART | 传感器采集、舵机控制、语音提示、执行动作 | ESP32 等嵌入式板子 |
模型层和交互层都在同一台 Windows 电脑上,通过进程内的服务调用,不额外引入网络开销。MVC 站点对外提供两类入口:一类是给人用的网页对话界面,另一类是给下位机或外部程序调用的 HTTP API。下位机怎么和大脑通信?我选了串口——最简单、最可靠,Windows 上用 COM 口直接收发,嵌入式端用 NanoFramework 的 SerialPort 类,两边都是 C#,代码风格统一,调试的时候心智负担很小。
为什么要分成三层而不是把模型直接塞进嵌入式设备?因为当前主流开源模型哪怕是量化到 1B 参数,也要几百 MB 内存和持续的计算能力,普通单片机完全跑不动;而把全部控制逻辑塞进 PC 又不现实,机器人的传感器和舵机必须由实时性强的下位机管。所以“PC 管脑、单片机管手”是最合理的分工。
1.3 模型格式选型:.gguf 与 .onnx 到底用哪个
项目标题里提到了两种格式,最终我在系统里两个都支持,因为各有各的适用场景。.gguf 是 llama.cpp 生态的标准格式,把模型权重、量化数据、分词器和超参数打包成一个文件,特别适合 llama.cpp 系推理引擎。.onnx 则是开放的神经网络交换格式,配合 ONNX Runtime 可以在不同硬件后端之间切换。
我的选型逻辑是这样的:
- 需要跑 7B 级别、追求对话质量、有英伟达显卡加速 → 选 .gguf,配合 LLamaSharp + CUDA 后端,量化后 4~5 GB,离线对话质量明显更好。
- 模型要部署到资源更紧的机器,或后续打算移植到其他平台 → 选 .onnx,模型体积小,ONNX Runtime 的交叉平台能力更成熟;但 ONNX 生态里适合对话的生成模型不如 GGUF 生态丰富。
- 两者我都保留的原因,是开发期可以一边在 LLamaSharp 上调提示词,一边用 ONNX 小模型验证端到端链路,互不阻塞。
格式本身不决定模型聪明不聪明,决定的是“这个模型在这个运行时里能不能方便地跑起来”。做选型时不要纠结“哪个格式更强”,要看你选的推理库和硬件支持哪个格式更顺畅。
2. Windows 推理底座:LLamaSharp 与 ONNX Runtime 双线落地
2.1 用 LLamaSharp 跑 .gguf:从 NuGet 到第一次出文字
LLamaSharp 是 .NET 社区里对 llama.cpp 的绑定封装,用起来和原生 C 库不完全一样,但胜在不需要写任何 C/C++ 代码,全部在 C# 里完成。第一步是建一个控制台或类库项目,装三个包:LLamaSharp、LLamaSharp.Backend.Cpu(纯 CPU 跑)或LLamaSharp.Backend.Cuda12(N 卡加速)。
模型文件去哪找?去开源模型平台下载一个指令微调版的 GGUF 文件就行。以 7B 量级的模型为例,Q4_K_M 量化版大约 4.4~4.7 GB,家用电脑能接受。下载后放在固定目录,代码里写死路径或者存到配置里。
真正跑推理的代码很短,核心流程是:加载权重 → 创建上下文 → 创建执行器 → 灌提示词 → 流式取回文字:
using LLama; using LLama.Common; var modelPath = @"D:\Models\qwen2.5-7b-instruct-q4_k_m.gguf"; var parameters = new ModelParams(modelPath) { ContextSize = 4096, GpuLayerCount = 20, // 有独立显卡就尽量多放几层,没有就设 0 BatchSize = 512, Seed = 42, UseMemoryMmap = true }; using var weights = LLamaWeights.LoadFromFile(parameters); using var context = weights.CreateContext(parameters); var executor = new InteractiveExecutor(context); var prompt = "你是一个学伴机器人,请用三句话介绍光合作用。"; var inferenceParams = new InferenceParameters { Temperature = 0.7f, MaxTokens = 256, AntiPrompts = new List<string> { "用户:", "</s>" } }; await foreach (var text in executor.InferAsync(prompt, inferenceParams)) { Console.Write(text); }几个参数值得展开讲。ContextSize是模型能“记住”的上下文窗口大小,7B 模型通常支持 4K~8K,但实际要根据内存设置。GpuLayerCount控制多少层放到显卡上算,这个值直接影响显存占用:放满 20 层可能吃掉 6~8 GB 显存,显存不够就往下调,让一部分层回落到 CPU。Temperature控制随机性,学伴场景我习惯设 0.6~0.7,太低会变复读机,太高容易胡说八道。
提示:首次加载 7B Q4 模型,内存至少预留 8 GB,纯 CPU 跑时速度约 8~15 token/s。别嫌慢,问答场景这个速度够用;如果追求流畅,建议配一张显存 8 GB 以上的显卡。
2.2 用 ONNX Runtime 走 .onnx:适合小模型的另一条路
小模型场景我走 ONNX Runtime 的生成式 API。这里要用到两个包:Microsoft.ML.OnnxRuntime和Microsoft.ML.OnnxRuntimeGenAI。前者是通用推理引擎,后者专门处理生成式模型的分词、采样和迭代推理。
ONNX 版的对话模型一般也是指令微调过的,通常几百 MB 级别。加载方式比 GGUF 更“黑盒”一些,不太需要自己配采样细节:
using Microsoft.ML.OnnxRuntimeGenAI; using var model = new Model(@"D:\Models\phi3_mini.onnx"); using var tokenizer = new Tokenizer(model); using var generatorParams = new GeneratorParams(model); generatorParams.SetSearchOption("max_length", 256); generatorParams.SetSearchOption("temperature", 0.8f); var tokens = tokenizer.Encode("你是生活机器人,请回答:如何用一句话安慰考试失利的人?"); using var generator = new Generator(model, generatorParams); generator.AppendTokenSequences(tokens); while (!generator.IsFinished()) { var sequence = generator.GenerateNextToken(); Console.Write(tokenizer.Decode(sequence)); }这套 API 和 LLamaSharp 的流式写法规整度差不多,都是一个 while 循环逐个吐 token。需要注意SetSearchOption里有些参数名是字符串,“max_length”“temperature”这些拼错了不报编译错误,运行时才报,排查时要细心。
模型层的设计我建议做一层薄的抽象接口,比如ILlmInference { IAsyncEnumerable<string> ChatAsync(string prompt); },GGUF 和 ONNX 各写一个实现类,由配置切换。这样 MVC 层完全不关心底层跑的是哪种格式,后续换模型、换推理后端都是改一行注册代码的事。
2.3 选模型与量化的实战心得
模型怎么选,直接影响整机体验。我踩过不少坑,总结出一条非常朴素的原则:在“参数足够小、量化后内存装得下、指令遵循能力够用”三个约束下选最大号的。
量化等级对照表如下,实测感受写出来供参考:
| 量化格式 | 7B 模型文件大小 | 体感质量 | 适用场景 |
|---|---|---|---|
| Q8_0 | 约 7.5 GB | 接近原版 | 内存/显存充裕,追求质量 |
| Q5_K_M | 约 5.0 GB | 很好 | 综合推荐,质量与体积平衡 |
| Q4_K_M | 约 4.4 GB | 良好 | 我的首选,资源紧张时也够用 |
| Q3_K_M | 约 3.3 GB | 一般 | 只跑测试链路,别用于正式对话 |
7B 模型 Q4_K_M 是我最常用的配置,它在流畅度和质量之间卡得比较好。更小的 1B~3B 模型跑起来飞快,适合做下位机指令分类、意图识别之类的辅助任务;但如果只有一个小模型当主力对话大脑,用户很快会发现它“智商不够”,体验会打折扣。另外,同一款模型不同量化版本之间,不要只盯着体积,实测对话质量才是最关键的——有时候 Q5 版本多出来的 0.6 GB 换来的是明显更少的“答非所问”。
模型下载后建议放到一个固定目录,比如D:\Models,并给每个文件建一个说明文本记录下载日期、来源、量化类型。这一步很多人忽略,三周后你管理十几个模型文件时就会感谢当初的随手记录。
3. ASP.NET MVC 把语言模型包成“会说话的服务”
3.1 接口设计:同步问答接口与流式输出的取舍
模型层搞定以后,第二步是让这个“大脑”对 Web 和外部程序开口。我建了一个标准 ASP.NET MVC 项目,Controller 里提供两个核心接口。第一个是常规的同步接口,适合下位机或其他系统调用,一次请求拿回完整回复:
[HttpPost] public async Task<JsonResult> Chat([FromBody] ChatRequest request) { var reply = await _llmService.AskAsync(request.Message, request.SessionId); return Json(new { ok = true, reply = reply }); }第二个是流式接口,给网页对话界面用。这里我用了 Server-Sent Events(SSE)而不是 WebSocket,因为 SSE 只需要单向持续推送,前端用EventSource就能接,实现成本远低于 WebSocket 的握手和重连逻辑。Controller 里把模型吐出来的每个 token 实时写进响应流:
[HttpGet] public async Task ChatStream(string message, string sessionId) { Response.ContentType = "text/event-stream"; Response.Headers.CacheControl = "no-cache"; await foreach (var chunk in _llmService.AskStreamAsync(message, sessionId)) { await Response.WriteAsync($"data: {chunk}\n\n"); await Response.Body.FlushAsync(); } }为什么需要流式?因为本机推理速度远不如云端大模型,7B 模型生成两三百字的回答可能要好几十秒。如果做成同步接口,用户面前就是一个转圈的白屏,体验极差。流式输出让用户看到“打字机”式的回复过程,体感延迟一下子小了很多。这个体验优化在本地模型项目里几乎是必须的。
3.2 单实例模型的并发控制与会话管理
本地模型不像云端 API 那样天然支持高并发,底层引擎一次只能跑一个推理,多了会互相争抢资源甚至崩溃。MVC 是多线程的,如果同时来了几个请求,不做控制就会同时挤进模型推理。我用一个简单的信号量把推理串行化:
public class LlmService : ILlmService { private readonly SemaphoreSlim _inferenceGate = new(1, 1); private readonly Dictionary<string, List<string>> _sessions = new(); public async Task<string> AskAsync(string message, string sessionId) { await _inferenceGate.WaitAsync(); try { var history = GetSessionHistory(sessionId); var prompt = BuildPrompt(history, message); var reply = await _inference.ChatAsync(prompt).LastOrDefaultAsync(); AppendToSession(sessionId, message, reply); return reply; } finally { _inferenceGate.Release(); } } }并发请求来了以后会排队,直观表现就是第二个用户要多等一会儿。这个设计符合项目定位——学伴机器人通常是一对一或一对少数人使用,排队可接受;比起崩溃,排队是优雅得多的问题。如果未来要支持真正的多路并发,要么换更强的硬件跑多实例,要么引入消息队列把推理任务异步化,这就超出当前项目范围了。
会话管理也是容易被忽略的点。模型是无状态的,每轮对话都要把之前的对话历史拼进提示词,机器人才“记得”你们聊过什么。我在服务里维护一个Dictionary<string, List<string>>,按 sessionId 存历史。上下文也不能无限增长,超过一定轮次就把最老的对话丢掉,给新对话留出空间——这对应模型层的ContextSize限制,4096 上下文大概能容纳 5~8 轮完整对话。
3.3 提示词模板与“学伴”人设的工程化
同一个模型,会不会“扮演”学伴,差别全在提示词。我的做法是把系统提示词单独抽成一个配置文件,不写死在代码里,方便随时调:
你是“小伴”,一款离线学伴生活机器人。 你的用户是中小学生。 回答要求: 1. 使用简体中文,语气亲切但不幼稚; 2. 理科问题优先给步骤和思路,再给答案; 3. 遇到不懂或不确定的,直接说明,不编造; 4. 回答控制在 150 字以内; 5. 日常闲聊时可以反问用户,引导对话。这条系统提示词我调试了很多版。最关键的教训是:不要期待模型自动“变聪明”,而是把约束写得越具体越好——字数、语气、回答结构都要明确。之前我写“请做一个负责任的学习助手”,模型完全不知道什么叫负责任;改成上面这种带编号的清单后,输出质量立刻稳定了。
Razor 页面本身不需要太复杂,一个消息列表、一个输入框、一个EventSource监听流式回复即可。页面上还可以加一个“模型状态”面板,显示当前加载的模型名、上下文大小、剩余内存,方便调试时一眼看出资源占用。这部分别花太多时间美化,功能跑通后把精力留给联调——后面那部分坑才多。
4. 下位机/嵌入式端:.NET NanoFramework 的通信与控制
4.1 为什么选 NanoFramework:和 .NET 一套 C# 打通
以前写嵌入式,大家默认用 C/C++,寄存器和中断满天飞;后来可以用 MicroPython,但是类型松散、性能和实时性都一般。这个项目之所以用 .NET NanoFramework,核心原因是“语言和工具链的统一”:上位机是 C#,下位机还是 C#,两边共享一套数据类型和序列化逻辑,协议设计完全不用跨语言对齐。
NanoFramework 是 .NET 在微控制器上的精简实现,支持 ESP32、STM32 等常见板子。它保留了System.IO.Ports.SerialPort、System.Device.Gpio这类 API,写起来和桌面 .NET 非常接近。对会 C# 的开发者来说,上手成本几乎为零;而对团队来说,维护一个纯 .NET 的机器人项目,比维护 C/C++ 和 C# 混合代码库要省心得多。
代价是什么?NanoFramework 的 API 子集比完整 .NET 小,比如没有 LINQ 的完整实现、泛型支持受限、字符串处理能力弱一点。这意味着协议解析和业务逻辑要写得保守一些,尽量用基本数组和简单循环完成。说实话,这对一个串口协议的解析来说完全够用。
4.2 串口帧协议设计:粘包与 CRC 校验
上下位机之间我设计了简洁的二进制帧协议。没用 JSON,因为嵌入式端的 JSON 库解析开销大,而且二进制协议在单片机侧更容易写的稳定。帧格式如下:
帧头 0xAA 0x55 | 长度(1字节) | 类型(1字节) | 载荷(N字节) | 校验 CRC16(2字节) | 帧尾 0x0D 0x0A类型字段区分了下位机上报和上位机下发两大类:0x01 表示 PC 发给机器人“说话”指令,载荷是 UTF-8 编码的文本;0x02 表示机器人上报传感器状态;0x03 表示执行动作指令。CRC16 校验放在靠近帧尾的位置,接收端收完一整帧后校验,校验失败直接丢弃一帧,等待下一帧重新对齐。
拆包是嵌入式通信最容易出问题的环节,串口数据会粘包、会半包。正确写法是维护一个环形接收缓冲区,一字节一字节地找帧头,找到后再按长度字段判断是否收齐,收齐后做校验和处理,而不是每次DataReceived都当完整数据用。NanoFramework 的串口事件写法如下:
using System.IO.Ports; var port = new SerialPort("COM3", 115200, Parity.None, 8, StopBits.One); port.DataReceived += OnDataReceived; port.Open(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { var bytes = new byte[port.BytesToRead]; int len = port.Read(bytes, 0, bytes.Length); buffer.AddRange(bytes); // 压入帧缓冲 TryParseFrames(); // 循环解析完整帧 }TryParseFrames内部就是一个状态机:找帧头、读长度、收载荷、验 CRC、处理,然后重复。这个函数我再提醒一次:必须在循环里把缓冲区的完整帧全部处理完,不能只处理一帧就返回,否则串口持续来数据时积压会越来越严重。
4.3 控制 GPIO、舵机与屏幕的实际写法
机器人要“动手”,下位机就要控制硬件。NanoFramework 的 GPIO 和 PWM API 与 .NET IoT 库保持一致,控制一个 LED、一个舵机、一个 OLED 屏都相当直接:
using System.Device.Gpio; using System.Device.Pwm; var gpio = new GpioController(); gpio.OpenPin(2, PinMode.Output); gpio.Write(2, PinValue.High); // 点亮状态灯 var servo = PwmChannel.CreateFromPin(4, 50); // 50Hz 舵机信号 servo.Start(); servo.DutyCycle = 0.075; // 舵机转到中间位置舵机的DutyCycle是个容易踩坑的点。普通舵机要求 20ms 周期(50Hz),对应的高电平脉冲宽度约 0.5ms~2.5ms,换算成占空比就是 2.5%~12.5%。我上面写的 0.075 正好是中间位置附近。如果你直接写 50% 占空比,舵机会疯狂转到极限并堵转,这是新手的典型事故现场。
“说话”这个功能不一定需要真接喇叭,很多学伴机器人是先让 PC 端用系统 TTS 合成语音播放,下位机只做表情和动作配合。我在下位机上用一个 I2C 小屏显示当前状态,收到 PC 发来的文本时,先做一个“正在思考”的动画,等动作指令来了就执行点头、摇头等舵机动作。这样整机体验已经很完整,暂时不碰语音合成的边缘情况。
5. 端到端联调:让机器人真正“开口、动手、回应”
5.1 完整数据流与各环节延迟预算
端到端联调是项目里最耗时间但最有成就感的一步。完整的数据流是这样的:用户对着网页或机器人按下按钮 → 触发提问 → ASP.NET MVC 收到请求 → 服务层拼提示词、取历史 → 模型推理产出一个 token 一个 token → 完整回复被流式推回网页 → 同时经过串口协议封装下发到下位机 → NanoFramework 解析指令,控制屏幕显示和舵机动作 → 完成后下位机回传状态帧 → PC 端记录日志。
其中每一段都有延迟。我在联调时给整机做了一个延迟预算:
| 环节 | 预留时间 | 说明 |
|---|---|---|
| 模型推理(7B Q4,CPU) | 10~30 秒 | 主要瓶颈,无法绕开,用流式缓解体验 |
| 串口下行 | < 50 ms | 115200 波特率传几百字节几乎瞬间完成 |
| 下位机动作执行 | 0.5~2 秒 | 舵机转动需要物理时间 |
| 网页渲染 | < 100 ms | 本地站点,几乎无网络拥塞 |
串口是最快的环节,基本可以忽略。做延迟预算的意义在于,联调时如果整体“变卡”了,你先看一眼卡在哪一段,不要一上来就怀疑模型速度。
5.2 联调步骤与验证清单
联调我建议按“先通链路、再加效果”的顺序做,千万别一开始就追求完整体验。
第一步,验证上位机到模型的链路。用控制台程序直接调用ILlmService,手动输入问题、打印回复,确认模型本身工作正常。第二步,跑通 MVC 接口。用浏览器访问网页或直接用 REST 工具 POST 一段消息,确认返回 JSON 正常、SSE 能持续推送。第三步,验证串口链路。上位机写一个测试脚本向下位机发一条 0x01 帧,下位机收到后回一条 0x02 帧,确认二进制协议收发正确。第四步才把所有环节串起来:网页提问 → 模型回复 → 同时下发动作指令 → 机器人动起来并回报状态。
每一步都用日志把关键数据打出来。上位机日志写到文件,下位机日志在串口调试终端查看。协议层的收发日志尤其重要,两边的时间戳能直接看出粘包、丢帧和解析错位问题。
5.3 延迟优化与异常兜底的经验
联调时最大的体验瓶颈是模型推理速度。我做了三件事让最终体验可接受。第一,把模型的拒绝回答、问候语等高频短回复缓存到内存里,同样的开篇语不再重复推理。第二,在网页端提前渲染“机器人正在思考”的动画,配合流式输出,用户等待的心理感知大大降低。第三,把系统提示词里的字数上限从 200 字压到 150 字,回复短了,生成时间自然少了三分之一左右。
异常兜底也要提前设计。模型偶尔会输出空内容或触发停止词后没有正常结束,我加了超时保护:单次推理超过 60 秒强制取消,返回“我有点卡住了,请再问一次”。下位机长时间没回报状态时,上位机重发一次指令,并做连续三次重试告警。这些兜底逻辑听起来简单,但真实场景里几乎一定会用到——模型不是服务器,它不会保证 100% 按预期输出。
6. 常见问题与排查技巧实录
6.1 模型侧的典型故障
先说模型加载失败。最常见的原因是内存不足,7B Q4 模型加载时需要一次性申请约 6~8 GB 的连续内存空间,如果电脑同时开了浏览器和开发工具,很容易加载到一半进程被杀。解决办法是关掉不用的程序,或者换 3B 小模型试跑链路。
其次是显存不足。GpuLayerCount设太高,CUDA 初始化直接报错。我排查这类问题的固定套路:先把GpuLayerCount设为 0,确认 CPU 能跑通,再逐步上调层数,每次加 5 层,找到当前显存的临界点。这样调虽然慢,但不会出现“一上来就崩”然后不知道崩在哪的情况。
还有上下文越长,推理越慢。7B 模型在上下文近满的时候,每个 token 的生成速度可能掉到最初的一半以下。对话历史超过 5 轮时我会主动截断最旧的对话,宁可让模型“短时记忆”,也不让推理速度不可接受。
6.2 串口与下位机侧的典型故障
串口最常见的是 COM 口被占用。Windows 下一切正常,但串口调试终端还开着,NanoFramework 的代码就打开失败。项目里统一约定:调试时只能有一个程序占用 COM 口,调试完记得释放。
粘包和半包问题更是高频。第一次联调时我直接按DataReceived里的字节数当作完整消息解析,结果 10 条里 3 条错乱。改成环形缓冲 + 状态机解析后彻底稳定。经验是:写协议解析时把“边界情况”作为第一优先级,正常情况反而简单。
下位机另一个常见问题是供电不足。舵机转动瞬间电流很大,用 USB 口直接供电会掉电重启。后来我给下位机单独接了一路外接电源,PC 串口只做数据通信,再也不随机重启了。这个坑看着像软件 bug,实际是硬件供电,排查起来特别容易绕远路。
6.3 一份可以照抄的排查速查表
| 现象 | 排查优先级 | 常见原因 | 处理建议 |
|---|---|---|---|
| 网页一直转圈无回复 | 先看模型服务 | 模型推理被排队或超时 | 查看服务日志,确认是否有并发卡死 |
| 模型加载即崩溃 | 看内存占用 | 内存不足或显存不足 | 换小模型或调低 GpuLayerCount |
| 回复到一半停止 | 看停止词 | AntiPrompts 设置不当 | 调整停止词列表,加入换行和特殊 token |
| 下位机无动作 | 看串口帧日志 | 协议解析失败或 CRC 错 | 打开收发日志,看帧头是否对齐 |
| 下位机随机重启 | 看供电 | 舵机瞬间电流过大 | 外接电源单独供电 |
| 舵机转到底堵转 | 看占空比 | 脉冲宽度错误 | 确认 50Hz 下占空比在 2.5%~12.5% 区间 |
| 对话“忘事”明显 | 看上下文 | 历史截断过猛 | 增大 ContextSize 或调高历史保留轮数 |
这张表是我在实际调节过程中整理出来的,基本覆盖了项目 90% 的故障。拿到问题先按优先级查,别从下往上翻,很多故障其实层层有关联——比如网页转圈,根子可能只是上下文太长导致推理慢,而不是接口写错了。
我个人在实际操作中最深的体会是:这种软硬结合的项目,最值钱的部分不是某一个技术点,而是“分层边界”设计得好不好。模型层、服务层、下位机层各管各的,每层只通过明确接口通信,联调时才能把问题定位到具体层。最后再分享一个小技巧:开发期建议把下位机回传的原始帧和上位机解析后的可读日志同时打印出来对照,录一段 5 分钟的日志文件,几乎能解决所有“时好时坏”的诡异 bug。这个项目后续还可以继续扩展——比如给下位机加语音合成、给 MVC 加权限管理、把模型层换成更好的量化版本,但骨架不出意外的话,短期内不需要大改。