统一大模型API接入:用litellm搭建模型网关的实战指南
最近在做 AI 应用的时候被各家大模型 API 的差异折腾得够呛。同一个功能OpenAI 的调用方式和 Anthropic 不一样换到本地模型又是另一套参数代码里到处都是 if-else 分支维护起来非常痛苦。后来我在一个开源社区里看到有人提到 litellm说它能把所有模型 API 统一成 OpenAI 格式当时半信半疑。直到我实际把它接入到项目里跑通全流程才发现这玩意儿确实能省掉一大半的对接工作。这篇文章不是官方文档的翻译是我自己从零开始接入、踩坑、排错之后总结出来的实战经验。我会从它到底解决什么问题讲起再到具体怎么装、怎么配、怎么在生产环境里用起来最后聊聊我遇到的那些坑和我的处理方式。如果你也在做多模型接入或者打算在公司里做统一的模型网关这篇文章应该能帮你少走不少弯路。1. 这个东西到底解决了什么问题先说个我真实的经历。之前我维护一个内部工具需要同时接入三家大模型的接口每家的鉴权方式不一样、消息格式不一样、参数命名不一样连超时重试的逻辑都得各写一套。最难受的是产品经理某天过来说下个版本我们想把默认模型换成另一个厂商的我一听就知道这意味着要改底层封装、改请求构造、改错误处理还要重新测一遍所有流式输出。这种重复劳动真的是纯内耗。litellm 的定位就是解决这类问题。它本质上是一个大模型 API 的兼容层和网关对外它提供一个统一的 OpenAI 格式接口对内它帮你把请求翻译成各家厂商的格式。你只要按照 OpenAI 的规范写一次代码剩下的模型切换、参数映射、错误处理、重试逻辑全部由 litellm 替你完成。它的核心价值可以归纳成下面几点一套代码接入 100 家模型提供商。从主流的几家大厂到各种开源模型的自托管服务基本都在支持列表里支持 SDK 调用和代理服务器两种模式。SDK 适合直接嵌入代码代理模式适合团队内统一收口 API 调用自带密钥管理、成本追踪、限流、重试、缓存等生产级能力相当于给你的 AI 调用加了一层技术中台那它适合谁用如果你是独立开发者想在几个模型之间快速切换做对比实验或者你在一个团队里负责维护公共的模型接入层又或者你们公司要做模型网关统一管理所有业务线的 API 调用litellm 都值得认真看看。不过在往下走之前我要先泼一盆冷水如果你们的业务只用一家模型的官方 API没有任何切换需求那其实没什么必要引入 litellm。多一层封装就意味着多一层学习成本和排查复杂度这一点大家要心里有数。2. 核心设计拆解SDK 和代理两条路我第一次看 litellm 的文档时有点懵因为它同时提供了两种使用方式很多人不知道该选哪个。我花了一段时间把两种都用了一遍现在可以很清楚地跟你说它们的区别。2.1 SDK 模式轻量接入适合直接嵌入SDK 模式就是直接在 Python 代码里调用litellm.completion()来完成请求。你可以传入任意一个模型标识符比如gpt-4o、claude-3-5-sonnet-20241022、ollama/llama3.2litellm 会在底层解析这个标识符自动确定对应的 API 地址、鉴权方式和请求格式。import litellm response litellm.completion( modelgpt-4o, messages[{role: user, content: 你好请介绍一下你自己}] ) print(response.choices[0].message.content)这里有个很关键的设计模型名里包含提供商的信息。比如ollama/llama3.2中间的斜杠前缀ollama就是提供商标识litellm 根据它来选择适配器。如果是 OpenAI 的模型直接写gpt-4o就行因为 OpenAI 是默认提供商。SDK 模式最大的好处是轻一行安装、几条调用就能跑起来。缺点也不难想到如果多个项目都要接模型每个项目都得配置各自的密钥环境变量密钥管理还是散落在各处没法统一收口。2.2 代理模式统一收口适合团队协作代理模式则是一个完整的 HTTP 服务。你启动一个 litellm proxy server所有业务系统都把请求打到这个服务上由它再转发到实际的大模型厂商。业务方只需要知道这个网关的地址不需要关心背后到底是哪家模型也不用持有各家厂商的密钥。litellm --config config.yaml --port 4000跑起来之后你用 OpenAI 的官方 SDK 把 base_url 指到http://localhost:4000就能正常调用from openai import OpenAI client OpenAI( api_keysk-any-string, base_urlhttp://localhost:4000 ) response client.chat.completions.create( modelgpt-4o, messages[{role: user, content: 你好}] ) print(response.choices[0].message.content)注意这里我随意填了一个sk-any-string当 api_key因为代理模式下 litellm 自己管理密钥校验上面这个写法要配合虚拟密钥功能才安全后面我会专门讲。代理模式的核心优势是集中管控。所有密钥集中在服务端配置业务方不接触真实密钥你可以对每个接入方分配不同的虚拟密钥做访问控制和额度限制所有请求的日志、成本、延迟都能统一记录和分析。这是它在生产环境里最受欢迎的原因。3. 配置与实操从一个可用配置说起3.1 安装与认证配置安装比较简单核心依赖是 Python 3.8 以上环境。我推荐用虚拟环境装避免把系统环境搞乱。pip install litellm[proxy]安装完后需要配置厂商密钥。litellm 的密钥读取遵循一套优先级规则会依次检查环境变量、.env文件、配置文件里的environment_variables字段。最省事的是准备一个.env文件放到项目目录下OPENAI_API_KEYsk-xxxxxxxxxxx ANTHROPIC_API_KEYsk-ant-xxxxxxxxxxx GEMINI_API_KEYAIzaxxxxxxxxxxx这些环境变量名是 litellm 预定义好的约定各厂商的密钥必须用对应的名字它才能识别。如果你用的是自托管的模型服务对应的环境变量写法可以参考官方文档但大部分情况下就是{提供商名大写}_API_KEY这个模式。3.2 写一个最小代理配置代理模式的核心是一个 YAML 配置文件。下面这个配置是我在实际项目里的最小可用版本我把模型列表精简成两家做演示model_list: - model_name: gpt-4o litellm_params: model: gpt-4o api_key: os.environ/OPENAI_API_KEY - model_name: claude-sonnet litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY litellm_settings: drop_params: true set_verbose: false general_settings: master_key: sk-master-123456拆开解释几个关键点model_list是你对外暴露的模型目录。model_name是业务方调用时使用的模型别名可以随意定义litellm_params.model才是真正映射到厂商的具体模型标识符。这种对外别名和真实模型分离的设计非常实用切换底层模型时业务方代码完全不用改。drop_params: true这个参数很值得留意。不同厂商支持的参数集不同比如有的模型不支持frequency_penalty如果不加这个配置传了不支持的参数请求就会直接报错。开启drop_params后litellm 会忽略那些目标任务不支持的参数虽然牺牲了一部分参数控制力但兼容性会好很多。master_key是代理服务的总管理员密钥。它的作用是保护管理接口比如后续要调用/key/generate生成虚拟密钥、查看/spend/logs花费日志都需要这个密钥。实际生产环境里要把它换成强随机字符串别跟我示例里一样写好记的。3.3 代理启动与验证流程配置文件准备好后启动litellm --config config.yaml --port 4000 --num_workers 4num_workers控制进程数这个要根据服务器 CPU 核数和预期并发量调整。我一般先设为 CPU 核数两倍然后再用压测数据去微调。启动之后可以先做个快速健康检查curl http://localhost:4000/health/liveliness然后模拟一个真实的 OpenAI 兼容请求curl http://localhost:4000/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer sk-master-123456 \ -d { model: gpt-4o, messages: [{role: user, content: 说句话}] }能正常返回 JSON 响应就说明代理已经跑通了。4. 生产环境必须用起来的关键功能配置跑通只是第一步。我把 litellm 放进生产环境之前仔细研究了它的几个核心能力。这几个功能如果不掌握那真的只是把它当成一个简单的格式转换工具在用很浪费。4.1 虚拟密钥把访问权真正管起来代理服务如果直接把 master_key 发给所有业务方一旦有人泄露整个网关就裸奔了。litellm 提供了虚拟密钥机制让你可以为每个接入方单独生成密钥并限定这个密钥能访问哪些模型、每分钟最多调用多少次、总预算多少。curl -X POST http://localhost:4000/key/generate \ -H Authorization: Bearer sk-master-123456 \ -H Content-Type: application/json \ -d { models: [gpt-4o, claude-sonnet], max_budget: 100, rpm_limit: 60, metadata: {team: data-platform} }响应里会得到一个key字段把它发给对应的业务方他们拿着这个 key 请求http://localhost:4000就行。这样每个团队一把钥匙预算和限流都隔离清楚出问题时也能直接定位到是哪个接入方的流量异常。4.2 模型重试与超时策略大模型接口的稳定性说实话不算特别好网络抖动、服务过载的情况时有发生。litellm 在代理层面统一处理重试和超时比业务方各自做容错要省心得多。配置方式是在litellm_settings里加litellm_settings: num_retries: 3 request_timeout: 600 retry_after: 0.5num_retries是重试次数retry_after是两次重试之间的基础间隔。litellm 默认采用指数退避策略每次重试的等待时间会递增避免重试风暴打垮厂商接口。request_timeout要特别说一下流式请求的总时长可能很长如果设太短思考时间长的模型很容易触发超时设太长又可能导致连接长时间占用。我一般按业务场景分开处理普通对话 60 秒需要深度推理的场景单独建一个配置 600 秒。4.3 成本追踪与用量统计做技术中台的人最关心的就是成本litellm 在这块做得比较完整。它内置了花费计算引擎根据模型、输入输出 token 数自动计算每次调用的成本。curl http://localhost:4000/spend/logs \ -H Authorization: Bearer sk-master-123456能看到每个请求的模型、token 数、预估花费、调用方等信息。代理还会在响应头里返回x-litellm-model-cost之类的字段方便业务方实时获取成本数据。有一点要提醒成本计算依赖 litellm 内置的模型价格表如果模型列表里有它不认识的型号花费会被标记为无法估算。遇到这种情况需要手动在配置里补上价格映射。4.4 语义缓存省钱的利器如果你有大量重复请求的场景比如客服机器人频繁查询同一个常见问题litellm 的缓存功能能省下不少预算。它支持按请求内容做精确匹配或语义相似度匹配命中缓存就直接返回之前的结果不再转发给模型厂商。litellm_settings: cache: true cache_params: type: redis host: localhost port: 6379缓存后端支持本地内存和 Redis多实例部署时务必用 Redis否则每个实例各存一份命中率会很低不说还可能导致数据不一致。语义缓存需要额外配置嵌入模型来算向量相似度这个我一般默认不开因为嵌入计算本身也有成本完全取决于你的请求重复率有多高。5. 多模型路由和故障转移的玩法litellm 还有一套路由能力可以实现负载均衡、故障转移和区域优先级。这是它从格式转换工具走向流量调度平台的关键分层。5.1 同一个模型挂多个密钥做负载均衡如果你有多个厂商密钥希望请求分摊到所有密钥上避免单个密钥触发限流可以这样配置model_list: - model_name: gpt-4o litellm_params: model: gpt-4o api_key: os.environ/OPENAI_API_KEY_1 - model_name: gpt-4o litellm_params: model: gpt-4o api_key: os.environ/OPENAI_API_KEY_2两个条目对外都是gpt-4olitellm 会在它们之间轮询分发请求。这个场景在账号被限流时非常实用我在做批量数据标注时就靠它把多个账号的配额都用满。5.2 配置 fallback 降级策略比负载均衡更常用的是故障转移。比如主模型超时或返回 5xx 错误时自动降级到备用模型。注意这里我用了同一个模型的两个不同提供商来做演示router_settings: fallbacks: [ {gpt-4o: [claude-sonnet]} ]含义是当gpt-4o请求失败并重试耗尽后自动改发到claude-sonnet。这个配置的价值在于提高整体可用性。我在一次厂商大规模故障时实测过业务侧完全无感因为网关自动把流量切到了另一家整体可用性从 97% 提到了接近 99.5%。不过 fallback 是双刃剑。两个模型的能力差异可能导致响应质量波动而且成本结构完全不同所以我在生产环境里只对非核心业务配置了 fallback核心交易链路还是保持宁可失败也不能静默切换模型的原则。5.3 容器化部署注意事项大多数团队不会直接裸机跑 litellmDocker 部署是更常见的方式。我这边提供一份经过验证的部署思路docker run -d \ --name litellm-proxy \ -p 4000:4000 \ -v $(pwd)/config.yaml:/app/config.yaml \ -v $(pwd)/.env:/app/.env \ --restart always \ ghcr.io/berriai/litellm:main-latest用宿主机文件挂载的方式覆盖配置方便后续更新。这里要提醒一个坑容器内的时区默认是 UTC如果你要用成本报表按本地时间统计需要在 Docker 启动参数里加上环境变量TZAsia/Shanghai。生产环境建议至少跑两个副本前面挂一层负载均衡。litellm 本身是无状态服务缓存用 Redis 的话多副本部署没有会话一致性问题扩容缩容都很简单。6. 常见问题与排查经验速查我整理了一份我在实际使用中遇到的典型问题。这些问题在官方文档里不一定写得很直白但我相信不少人都踩过类似的坑。问题现象可能原因排查方法调用时报model not found请求里的模型名在model_list里不存在检查配置中对外model_name是否匹配注意大小写报错401 Unauthorized虚拟密钥无效或已被删除用 master_key 调/key/info查看该密钥状态请求超时频繁request_timeout设置太短查看请求日志确认耗时分布按 P95 调整超时某个模型返回参数不支持目标模型不支持 payload 里的某个参数开启drop_params或检查litellm_params是否多传了参数成本记录为 0模型不在 litellm 价格库中手动在配置里添加该模型的价格映射流式响应中断代理层或厂商层流式处理异常先直连厂商测试流式是否正常再对比代理转发密钥无法从环境变量读取变量名不匹配 litellm 约定确认环境变量名格式为{厂商大写}_API_KEY6.1 一个典型的流式调用问题有段时间我们一个业务方反馈用流式方式接 litellm 时响应总是中途断开。我排查了很久最后发现问题是出在代理层的num_workers上。当时我开的是 8 个 worker但每个 worker 默认的连接池大小不够在高并发流式请求时连接被占满新的请求坐等不到空闲连接就断了。后来通过配置文件手动调大了连接池大小问题才解决。这种问题靠看应用日志很难发现因为你看到的是某个连接被重置但背后原因其实在更底层。6.2 关于密钥管理的一点建议虚拟密钥虽然好用但有个细节要注意删除密钥不会立即切断已经建立的连接在途请求还是会正常完成。所以需要轮转密钥时建议先创建新密钥、切换业务方配置、再删除旧密钥而不是直接杀掉旧密钥。我因为这个操作顺序吃过亏删得太急导致线上还有几十个请求跑在失效的密钥上。7. 我的一些收尾心得litellm 对我来说最大的价值不是省了那几行代码而是它把接入模型这件事从每个项目各做一遍变成了平台做一次所有项目复用。我们团队现在有五个业务模块通过它统一接模型新模型接入的时间从原来的一到两天压缩到了半小时以内基本上是改配置、测连通性、发布三步走。最后分享一个小技巧。如果你想快速评估一个新模型是否值得接入可以在代理配置里临时加一条model_name指向新的目标模型然后指定一个测试虚拟密钥只给这一个模型权限直接请业务方小流量试用。试用满意了再放开给全线业务试得不满意直接删掉配置对线上毫无影响。这种低成本试错的方式是用熟 litellm 之后带来的额外红利。如果你正准备给团队搭模型网关或者还在几个模型之间犹豫不决我建议不要急着在业务代码里写兼容层。先花一天时间把 litellm 跑通把你的真实场景在它上面走一遍再决定要不要深入使用。一个工具好不好用终究还是要靠自己的实际场景来验证。

相关新闻

基于PHP开发学生管理系统Web网站:从环境搭建到部署实战

基于PHP开发学生管理系统Web网站:从环境搭建到部署实战

简介:一套基于PHP构建的学生管理系统Web网站源码,面向教育机构、教师以及初中级PHP开发者,主要解决学生档案、课程、专业和管理员信息的高效维护问题。系统采用PHPMySQL实现数据存取,前端由HTML、CSS、JavaScript构成,…

2026/10/13 11:44:21 阅读更多 →
多变量线性回归实战:特征工程、梯度下降与调参避坑指南

多变量线性回归实战:特征工程、梯度下降与调参避坑指南

1. 从单变量到多变量:为什么房价预测不能只看面积 很多人学完单变量线性回归之后,都会有一种“我好像已经会了”的错觉。毕竟公式简单,梯度下降跑一跑,画条直线拟合数据,看起来挺像那么回事。但真正拿到一个稍微贴近现…

2026/10/13 11:44:21 阅读更多 →
微信小程序+Spring Boot+UniApp的大学生兼职推荐系统全栈开发

微信小程序+Spring Boot+UniApp的大学生兼职推荐系统全栈开发

微信小程序 springboot uniapp 的大学生兼职推荐系统,这个题目我盯了很久。说实话,大学生兼职类小程序市面上已经有不少,但真正把"推荐"两个字做扎实的并不多。多数产品就是做了一个职位列表加搜索,用户来了自己翻&am…

2026/10/13 11:44:21 阅读更多 →

最新新闻

关系模型实战指南:从二维表到业务稳定性的底层逻辑

关系模型实战指南:从二维表到业务稳定性的底层逻辑

1. 这不是教科书里的“关系”,而是你每天都在用的数据组织逻辑“关系模型的基本概念”——看到这八个字,很多人第一反应是大学数据库课上那个被粉笔灰笼罩的下午,黑板上写着“实体、属性、域、键”,讲台下有人偷偷刷手机&#xff…

2026/10/13 12:22:17 阅读更多 →
Qt国际化翻译完整实践:从tr()到动态语言切换

Qt国际化翻译完整实践:从tr()到动态语言切换

做Qt开发这么多年,遇到最多的问题不是功能写不出来,而是功能做完以后客户说看不懂。于是QT国际化翻译就成了绕不开的环节。表面上看,这事只是把界面文字换成英文、日文,真正做起来却涉及源码扫描、翻译文件管理、运行时加载、动态…

2026/10/13 12:22:17 阅读更多 →
华为+红帽双赛道:2026网工转型的破局之道

华为+红帽双赛道:2026网工转型的破局之道

1. 为什么2026年网工转型,我建议你押注"华为红帽"双赛道最近不少朋友私信问我关于2026年转行网络工程师(网工)的事,大家的焦虑点很一致:AI工具越来越强,初中级网工的活是不是要被替代&#xff1f…

2026/10/13 12:22:17 阅读更多 →
Modern.js 服务端编译核心 @modern-js/server-utils:TypeScript 编译、别名解析与原生 ESM 支持的源码级解析

Modern.js 服务端编译核心 @modern-js/server-utils:TypeScript 编译、别名解析与原生 ESM 支持的源码级解析

前端后端Web框架 【免费下载链接】modern.js A progressive web framework based on React and Rsbuild. 项目地址: https://gitcode.com/gh_mirrors/mo/modern.js 点击查看 免费下载 本文围绕 packages/server/utils/CHANGELOG.md 记录的能力演进,结合…

2026/10/13 12:22:17 阅读更多 →
基于SpringBoot+Vue的派出所健康档案管理系统设计与实现

基于SpringBoot+Vue的派出所健康档案管理系统设计与实现

选题背景 社会对公共安全的重视,派出所民警的工作压力日益增大,其健康状况备受关注。早期的健康档案管理方式效率低下,难以实现信息的实时共享和动态更新。本系统采用 Spring Boot 框架,结合前端技术和数据库技术,构建…

2026/10/13 12:22:17 阅读更多 →
EmbeddingGemma 2本地多模态嵌入模型部署与向量检索实战

EmbeddingGemma 2本地多模态嵌入模型部署与向量检索实战

1. 这个模型到底是什么:先把它的定位弄清楚拿到"EmbeddingGemma 2"这个名字,第一反应就是:这是个嵌入模型,740M参数规模,多模态,而且主打本地推理。这几个关键词放在一起,意味着它并不…

2026/10/13 12:21:16 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →