ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

本地大模型网关CLI实战:从Ollama到LiteLLM的终端统一入口

本地大模型网关CLI实战:从Ollama到LiteLLM的终端统一入口 1. 为什么你需要一个“本地大模型网关 CLI”先聊一个很实际的场景你在本地跑了一套大模型服务可能是 Ollama、LM Studio、vLLM 或者 llama.cpp日常调试时要么开浏览器对着 Web UI 点来点去要么在终端里敲一长串 curl 命令参数稍微复杂一点就得翻历史记录。等到模型换了好几个、API 地址改过几轮之后整个人都是崩溃的。我那时候的状态基本就是改个 temperature 都要先想半天这个参数到底该放哪一层--data 还是 --jsonheader 里到底要不要带 Authorization。后来我意识到真正缺的不是又一个“好看的模型管理面板”而是一个能在终端里快速调用、切换、调试本地模型的统一入口。这个入口就是“本地大模型网关 CLI”。你可以在自己的电脑上把网关跑起来再用 CLI 向网关发请求。所有模型都被网关收口管理CLI 只负责把请求发到网关上模型换没换、端口变没变CLI 完全不用感知。这样带来的好处非常直观命令变得极短模型切换不再需要改代码请求日志、速率限制、甚至多用户鉴权都能在网关这一层统一处理。这篇文章我用自己的实际踩坑经历把“本地大模型网关 CLI”从选型到落地讲一遍包括为什么用网关而不是直接连模型、CLI 命令怎么设计、本地部署要注意哪些细节、以及我后来遇到的几个奇怪问题是怎么排查的。适合正在折腾本地大模型、又被一堆参数搞得头疼、想在终端里获得干净体验的朋友。先说明一下下文涉及的具体命令和配置我以 LiteLLM 网关 一个我自封装的 Python CLI 为例展开。它们不是唯一选择但思路完全通用你换成其他网关或 CLI 工具也能套用。2. 网关 CLI 的整体思路拆解2.1 网关解决的核心问题多个模型一套入口以前我本地同时装了 Ollama 和 LM Studio分别跑不同的模型。每次要切换得记两个服务的端口和两套 API 格式。Ollama 是 /api/generateLM Studio 基本兼容 OpenAI 格式但细节上还是有差异。代码里封装了一层又一层改动一次模型就要动一次配置。网关的定位就是“模型的路由器”。你只需要记住网关的地址比如 http://localhost:4000所有模型都挂到它下面。向网关发请求时通过 model 字段指定要用哪个模型网关负责把请求翻译成目标模型能听懂的语言再转发过去。我选 LiteLLM 当网关核心原因是它对 OpenAI 格式兼容得非常好同时支持接入 Ollama、vLLM、llama.cpp、DeepSeek、通义千问等一堆后端。它不是那种侵入式的重平台就是一个轻量服务配置放在 config.yaml 里改完重启就能生效。model_list: - model_name: ollama-qwen litellm_params: model: ollama/qwen2.5:7b api_base: http://localhost:11434 - model_name: lmstudio-llama litellm_params: model: openai/llama3.1:8b api_base: http://localhost:1234/v1 api_key: fake-key这段配置的意思是我对外暴露两个模型名一个叫 ollama-qwen底层走 Ollama 的 qwen2.5 7B另一个叫 lmstudio-llama底层走 LM Studio 里的 llama3.1 8B。CLI 侧永远只跟这两个名字打交道。2.2 CLI 存在的意义让“调用”变成一件顺手的事有了网关之后你其实已经可以用 curl 发请求了。但 curl 的问题在于每次都恨不得写八九十行。尤其当你需要频繁测试不同温度、不同 system prompt、不同输出长度的时候一条命令改来改去很容易把参数搞混。CLI 的价值就是把“调用大模型”这件事浓缩成一段自然的终端操作。我自己想要的体验是这样的llm chat --model ollama-qwen --message 用三句话总结网关的作用或者更简单一点直接进入交互模式llm chat --model ollama-qwen然后像聊天一样来回输入。终端里没有浏览器标签页干扰没有一大堆 JSON 把视线挡住输入回车就能看到结果。对于写脚本、批处理、快速验证 prompt 的场景这种轻量方式比任何 Web UI 都高效。2.3 为什么不是“CLI 直连模型”非要中间夹一个网关这是我最开始纠结的地方既然 CLI 能直接请求 Ollama为什么还要多一层后来发现网关层的存在不是多余而是把几个隐藏成本一次性解决了。第一是统一鉴权。本地调试时无所谓但当你想着把能力开放给同一局域网的其他设备、或者跑在腾讯云轻量服务器上给远程终端用时网关可以统一挂 API KeyCLI 只需要配一份鉴权信息底层模型全都藏在网关后面。第二是请求日志。网关能看到所有请求的模型、耗时、token 消耗这在本地做 prompt 调优和成本估算时非常有用。第三是格式归一化。你不需要关心底层是 Ollama 还是 vLLMCLI 永远只发 OpenAI 格式剩下的事网关处理。代价是多一个进程多一点配置。当你的模型数量超过两三个以后这个代价完全值得。3. 核心细节解析CLI 与网关的协作原理3.1 一次请求的完整路径先跟着我走一遍请求链路这样后面配参数时你就知道每一行命令到底在打哪一层。终端输入命令 - CLI 读取参数模型名、消息、温度、最大 token 等 - CLI 组装成 OpenAI 格式的请求体 - POST 到网关 /chat/completions - 网关根据 model 字段查配置表 - 网关把请求改写为目标模型的原生格式 - 转发到 Ollama / LM Studio / vLLM 等后端 - 拿回结果后网关再统一转成 OpenAI 格式返回 - CLI 解析响应打印正文这里最关键的一步在网关的“格式改写”。比如 Ollama 原生字段叫 prompt、system而 OpenAI 格式里叫 messages。网关如果不做转换CLI 写的请求底层模型根本看不懂。这个活以前是代码里的封装层干的现在挪到了网关。3.2 CLI 的参数设计足够少又足够用我给 CLI 定参数时有个原则高频参数必须短低频参数可以稍微长一点但绝不能没有。最终保留的核心参数有这些参数示例作用--modelollama-qwen指定网关里的模型名--message你好单次对话消息--system你是一个翻译助手设置系统提示词--temperature0.7控制随机性--max-tokens2048限制最大输出长度--json无值以原始 JSON 形式打印完整响应--interactive无值进入交互式聊天模式--stream无值流式输出一边生成一边打印这些参数的解析我直接用 Python 标准库 argparse没有额外引入 click 或 typer因为这个工具本身不大标准库足够用少一个依赖就少一处维护负担。import argparse def build_parser(): parser argparse.ArgumentParser(descriptionLocal LLM Gateway CLI) parser.add_argument(--model, requiredTrue, helpModel name exposed by the gateway) parser.add_argument(--message, helpSingle user message) parser.add_argument(--system, default, helpSystem prompt) parser.add_argument(--temperature, typefloat, default0.7) parser.add_argument(--max-tokens, typeint, default1024) parser.add_argument(--json, actionstore_true, helpPrint full JSON response) parser.add_argument(--interactive, actionstore_true, helpInteractive chat mode) parser.add_argument(--stream, actionstore_true, helpStream output) return parser.parse_args()3.3 为什么默认值要这样定temperature 默认 0.7是因为大多数文本生成任务在 0.7 左右能兼顾创造性和稳定性。写代码可以降到 0.2做创意写作可以拉到 0.9但 CLI 的默认值应该是一个“不出错”的中间档。max-tokens 默认 1024是为了防止某些模型在后端配置有问题时无限生成把终端刷爆。system 默认空字符串。这个要特别注意不要因为“默认给一个系统提示词”显得更智能就去加本地模型对 system prompt 的敏感度差异很大默认给一个反而可能干扰模型表现。3.4 请求体组装严格走 OpenAI 格式CLI 向网关发送请求时请求体严格按 OpenAI 格式组装。messages 数组里只有 system 非空时才加入 system 消息否则只有 user 消息。这样可以避免多余字段影响网关的转发行为。def build_messages(system: str, user_message: str): messages [] if system: messages.append({role: system, content: system}) messages.append({role: user, content: user_message}) return messages def build_payload(model, messages, temperature, max_tokens): return { model: model, messages: messages, temperature: temperature, max_tokens: max_tokens, }这里有个容易犯的错把 max_tokens 拼成了 max_token或者忘了放 model。网关校验失败返回 400 的时候第一反应应该是检查这几个字段名。4. 实操过程从安装到跑通第一个对话4.1 环境准备与安装我的环境是 macOS Python 3.11但下面的步骤在 Ubuntu 服务器上同样适用只是包管理命令从 brew 换成 apt。整个链条分三段装网关、写 CLI、跑起来。pip install litellm[proxy]装完以后启动一个最简网关litellm --config config.yaml --port 4000如果一切正常日志里会出现类似Uvicorn running on http://0.0.0.0:4000的内容。这一步如果报错九成是 config.yaml 格式问题重点检查缩进。CLI 部分我没有单独做成 pip 包直接写成一个脚本文件llm.py然后配一个 shell 别名alias llmpython3 /path/to/llm.py。这样做的好处是改代码立刻生效不用反复安装。4.2 第一步验证网关连通性装好以后先不要急着写复杂功能先用 curl 打一发确认网关和后端模型之间链路通畅。curl http://localhost:4000/health返回{status:ok}就说明网关活着。再看模型列表curl http://localhost:4000/v1/models这一步能看到网关暴露出来的模型名比如ollama-qwen和lmstudio-llama。如果这里看不到预期模型名说明 config.yaml 里的 model_list 没配对回去检查名称拼写。4.3 第二步CLI 非交互模式跑通网关正常以后用 CLI 发第一条消息python3 llm.py --model ollama-qwen --message 你好介绍一下你自己如果一切正常终端会直接打印模型的回答前面没有任何多余 JSON。这一步的体验感非常强看到干干净净的文本输出时你会觉得之前那些 curl 里的 --data 都是浪费时间。如果只是想要原始响应做调试就加 --jsonpython3 llm.py --model ollama-qwen --message 你好 --json打印出来的是完整 JSON包含 usage 里的 prompt_tokens 和 completion_tokens方便估算成本。4.4 第三步交互式聊天模式非交互模式适合脚本调用但日常试 prompt 的时候一条一条敲命令还是麻烦。所以我给 CLI 加了交互模式实现逻辑很简单循环读取输入每次把用户输入加到 messages 数组完整发给网关再把结果打印出来。def interactive_loop(args, api_base, api_key): messages [] if args.system: messages.append({role: system, content: args.system}) print(Entering interactive mode. Type exit to quit.) while True: try: user_input input( ) except (EOFError, KeyboardInterrupt): break if user_input.strip().lower() in (exit, quit): break messages.append({role: user, content: user_input}) reply send_chat_request(api_base, api_key, args.model, messages, args.temperature, args.max_tokens, args.stream) print(reply) messages.append({role: assistant, content: reply})这里有个设计细节messages 数组会不断累积。也就是说整个会话的上下文一直是连续的模型能记住前面聊过的内容而不是每次都当新对话处理。这是交互模式相对非交互模式最重要的差异。4.5 流式输出让等待变得不焦虑非流式模式下请求发出后终端会一直卡住直到模型生成完才一次性输出。本地 7B 模型生成几百 token 还好如果跑到 13B 或者更大的模型等待时间会让人怀疑程序是不是卡死了。流式输出解决的就是这个问题。开启 --stream 后CLI 使用 requests 库的 streamTrue逐行读取服务端返回的 SSE 数据流每拿到一个 chunk 就立刻打印其中的增量文本。def stream_chat(api_base, api_key, model, messages, temperature, max_tokens): url f{api_base}/chat/completions headers {Authorization: fBearer {api_key}, Content-Type: application/json} payload build_payload(model, messages, temperature, max_tokens) with requests.post(url, jsonpayload, headersheaders, streamTrue) as resp: for line in resp.iter_lines(decode_unicodeTrue): if line: print(parse_stream_line(line), end, flushTrue) print()注意SSE 返回的数据格式是data: {...}每一行以 data: 开头。解析时要先把前缀剥掉再尝试 json.loads。还有一种情况是收到data: [DONE]那个是结束标记直接 break 就行。4.6 实战验证批处理场景除了聊天CLI 另一个高频用途是批处理。比如你有 10 条文本需要让模型做摘要手动一条条输入太累可以写一个小的批量循环for text in $(cat texts.txt); do python3 llm.py --model ollama-qwen --message 摘要$text --temperature 0.3 summaries.txt done这批脚本里的诀窍是 temperature 调低批量任务通常要求输出稳定不飘0.2~0.3 比默认的 0.7 稳得多。真实工作中我踩过一次坑有一个批处理任务忘了改 temperature结果 20 条摘要里有 3 条文本风格差异巨大排查了半天才意识到是随机性太高。5. 常见问题与排查技巧实录5.1 问题一CLI 报“Unable to locate the codex cli binary”这个话题我不得不提因为在 2025 年这个时间点关于“CLI”的搜索里总绕不开 Codex CLI、Claude CLI 这类 AI 编程工具。如果你在安装某些 AI Coding CLI 工具时看到unable to locate the codex cli binary or required runtime components之类的报错本质是安装过程没有把可执行文件放到 PATH 环境变量能找到的目录里。这个跟本文的“本地大模型网关 CLI”不是同一个工具但排查逻辑完全一致检查 PATH 里是否包含可执行文件所在目录检查二进制文件是否有执行权限检查安装脚本是否因为权限问题没有完整写入。which codex echo $PATH ls -l $(which codex)如果 which 找不到就说明 PATH 没配好。常见的安装位置是~/.local/bin检查一下这个目录是否在 PATH 里。这种情况在 macOS 和 Linux 上都很常见特别是用某些安装脚本时它把文件放进去了但没往 .bashrc 或 .zshrc 里追加路径。5.2 问题二网关返回 404 Model Not FoundCLI 请求没问题但服务端回应说找不到模型。这个问题的根源通常是 gateway 配置里模型名和 CLI 传入的 model 名称没对上。比如 config.yaml 里写的是model_name: ollama-qwen但你在 CLI 里手滑写成了ollama/qwen网关肯定找不到。排查思路很直接先用 curl 请求/v1/models看真实暴露的名字再对比 CLI 命令里的 --model 参数。5.3 问题三后端模型连不上网关报 Connection Refused这个更底层一些。网关进程活着但网关转发请求到 Ollama 或者 LM Studio 时目标端口连不上。最常见的排查方法是curl http://localhost:11434 # 测试 Ollama 是否在跑 curl http://localhost:1234/v1/models # 测试 LM Studio 是否在跑如果目标端口不通先去把对应的模型服务启动起来。还有一种隐蔽情况是Ollama 用 Docker 跑宿主机端口映射没加容器内部 11434 通但宿主机访问不到。这时候要去 Docker 配置里把端口映射加上而不是在网关层瞎调。5.4 问题四流式输出乱码或数据缺失流式输出时偶尔会遇到输出不完整、突然中断、或者打印出来一堆data: [DONE]这样的标记。我遇到过的原因有两个一是 SSE 解析逻辑没有把[DONE]单独处理把它当成 JSON 解析导致报错二是超时时间设置太短大模型生成速度慢请求被客户端主动断掉。解决方式是在 requests.post 时把 timeout 调大比如timeout(10, 300)。这里第一个数字是连接超时第二个是读取超时。本地模型有的跑得慢300 秒读取超时在这个场景是合理的不要被“设置长超时显得不专业”的错觉误导。5.5 问题五CLI 打印日志太多看不到模型输出这种问题通常不是 CLI 本身的问题而是你在请求库层面开了 debug 日志。requests 库如果开了logging.DEBUG会把每个 HTTP 请求的详细内容全部打印出来。排查时可以暂时把日志等级调到 WARNING或者直接注释掉。import logging logging.getLogger(requests).setLevel(logging.WARNING)6. 网关 CLI 的进阶扩展6.1 添加多个模型后端本地跑起来以后你一定会有加新模型的需求。加模型的流程很简单在网关的 config.yaml 里增一段配置然后重启网关。CLI 这边完全不用动只要你知道新模型的对外名称就行。比如我想加一个跑在 vLLM 上的模型- model_name: vllm-deepseek litellm_params: model: openai/deepseek-ai/DeepSeek-V2-Lite api_base: http://localhost:8000/v1 api_key: empty重启后立刻就能用python3 llm.py --model vllm-deepseek --message vLLM 模型的调用方式和之前完全一样这个特性是网关模式最有价值的点后端怎么换前端调用方毫无感知。6.2 将 CLI 封装成远程可用服务本地网关跑通之后你可能会想能不能在平板上也访问可以。网关监听 0.0.0.0:4000 即可前提是防火墙放行端口。然后 CLI 里的 api_base 不要写 localhost改成你电脑的局域网 IP。为了让配置更灵活我给 CLI 加了一个环境变量支持export LLM_GATEWAY_BASEhttp://192.168.1.100:4000然后在代码里读这个环境变量没有才回退到 localhost。import os api_base os.environ.get(LLM_GATEWAY_BASE, http://localhost:4000)这样你在手机终端 App 里配好环境变量一样能调用本地模型只是不要指望手机上的输入体验能比电脑好太多。6.3 CLI 的 prompt 模板化用久了你会发现很多请求的 system prompt 是重复的。比如“你是一个擅长 Python 的代码审查助手”“你是翻译引擎把输入翻译成英文”。与其每次敲一遍我直接把常用 prompt 存成了几个子命令的参数组合。实现方式是加一个 --preset 参数预设几个常见角色PRESETS { translator: 你是一个专业的翻译引擎将用户输入翻译成英语只输出翻译结果。, code-reviewer: 你是一个资深 Python 工程师请对以下代码进行严格审查指出潜在问题并给出修改建议。, summarizer: 你是一个文本总结助手用简洁的语言总结用户输入的核心内容。, }python3 llm.py --model ollama-qwen --preset translator --message 今天天气很好命令更短prompt 质量也更稳定不会出现因临时手打漏字导致的输出飘移。7. 关于“CLI 工具选择”的个人经验现在命令行 AI 工具特别多光我见过的就有 Codex CLI、Claude CLI、DeepSeek CLI、GitHub CLI 系每个人都在抢占“终端里的 AI 助手”这个入口。我自己的观念是这样的如果你是拿大模型做通用编程辅助那些大厂出的 CLI 确实集成度高开箱即用但它们大多是绑定自家云端 API 的。而本地大模型网关这套方案核心价值在于“不绑定任何一家云端服务”所有请求都跑在局域网自己的模型上。数据隐私、离线可用、完全可控这三点是本地方案最大的护城河。你可以同时接 Ollama 里的开源模型跑日常问答再在需要时临时把某个请求路由到云端 API 网关。这种自由度是单一 CLI 工具给不了的。关于“Codex CLI 和 Codex 哪个更好用”这类问题我的回答是如果你在本地跑私有模型做实验网关 CLI 的组合更灵活如果你就想要最开箱即用的 AI 编程体验那些官方 CLI 自然有它们的生态优势。不同场景选不同工具没必要非此即彼。8. 我实际使用中的一些体会这套方案我用了一个多月真正改变习惯的倒不是省了多少按键而是把大模型的调用从“沉重的工程操作”变成了“顺手的小命令”。以前我想比较两个模型对同一个问题的回答得先开两个浏览器页面分别切换模型、粘贴 prompt、截图保存。现在一条命令就完事python3 llm.py --model ollama-qwen --message 用一句话解释 TCP 三次握手 python3 llm.py --model lmstudio-llama --message 用一句话解释 TCP 三次握手两个结果并排在终端里差异一目了然。还有个细节值得说--system参数配合--temperature调低一点做结构化输出时非常稳定。比如让模型只能输出“是/否/不确定”三项0.1~0.2 的温度几乎不会跑偏。温度调低以后模型输出稳定但略显死板但大部分工程场景宁可要稳定的死板也不想要飘忽的花活。最后一个小技巧送给已经在折腾的朋友CLI 里别只想着聊天。试试把它嵌进自己的构建流程里比如写一个脚本让模型帮你总结 git diff、生成 commit message这种自动化的快乐是鼠标点击 Web UI 永远给不了的。这套方案真正的好处就是让你觉得终端里的一切都开始为你服务了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进