litellm 统一大模型调用:从接口适配到生产级模型网关实践
1. 从一个“模型调用地狱”说起litellm 到底解决了什么问题如果你最近半年在折腾大模型应用大概率会遇到这样一个场景项目一开始只想接一家模型服务代码写得干干净净一个 SDK 走天下。结果产品经理跑过来说能不能加个备用模型运营说这个场景用便宜的那家更划算老板又补一句海外用户走另一家延迟更低。于是你的代码里开始出现 if-else 分支每个分支对应一套 SDK、一套鉴权、一套参数格式、一套返回结构。三个月后这个文件变成了没人敢动的“祖传屎山”。litellm就是冲着这个痛点来的。一句话概括它是一个用统一接口调用上百种大模型服务的 Python 库同时提供代理服务让你用类似 OpenAI 的调用方式去访问不同厂商的模型而不用为每家单独写适配层。你写一次completion(modelxxx, messages[...])换模型只需要改model这个字符串。它解决的问题可以拆成三层。第一层是接口统一不同厂商的入参命名、消息格式、返回结构差异巨大litellm 在中间做了一层标准化。第二层是运维统一它自带一个代理服务可以把所有模型的密钥、限流、预算、日志集中管理前端应用只对着一个地址说话。第三层是成本与可靠性统一支持失败自动重试、跨模型降级、按 key 统计花费这些在真实生产里比“能调通”重要得多。适合谁来参考如果你是独立开发者想快速在多个模型间做 A/B 对比litellm 能省掉大量胶水代码如果你是团队里的后端需要给业务方提供一个稳定的模型网关litellm 的代理模式基本是开箱即用如果你在做 Agent、RAG 这类会频繁切换模型的项目它几乎是标配。下面我会从设计思路、核心细节、实操落地到踩坑排查完整讲一遍我是怎么用它把“模型调用地狱”收拾干净的。2. 整体设计思路拆解为什么是“统一层”而不是“再包一层 SDK”2.1 核心抽象把差异收敛到一个 model 字符串litellm 最核心的设计决策是把“用哪家模型”这件事压缩成一个字符串。比如gpt-4o、claude-3-5-sonnet、gemini-1.5-pro、ollama/llama3这种写法前缀或命名本身就携带了路由信息。库内部维护了一张映射表根据这个字符串决定用哪个适配器、走哪个 base_url、需要哪些必填参数。这个设计的好处在于调用方不需要知道底层是谁。你的业务代码里只有model这个变量它可以从配置中心来、从数据库来、从环境变量来。要做灰度改配置就行不用改代码、不用重新发版。这一点在快速迭代的团队里价值极高。对比一下另一种常见做法——自己封装一层if model_name a: client_a.chat() elif ...。这种写法在模型数量少的时候没问题但一旦超过三四个维护成本指数上升而且每家的重试逻辑、超时处理、流式解析都要重复写。litellm 把这部分公共逻辑抽走了你只需要关心业务。2.2 双形态设计库模式与代理模式的分工litellm 提供两种使用形态这个设计很值得说。库模式SDK直接pip install litellm在 Python 代码里 import 调用。适合脚本、Notebook、单体应用。优点是零部署成本缺点是密钥散落在各个服务里不好统一管控。代理模式Proxy Server启动一个独立服务对外暴露 OpenAI 兼容的 HTTP 接口。所有应用都请求这个代理代理再去调真实模型。适合多服务、多团队协作的场景。密钥只存在代理这一处还能统一做限流、预算、审计。我的建议是个人项目和小团队先用库模式快速验证一旦涉及多人协作或需要成本管控立刻切到代理模式。两者共享同一套底层适配逻辑迁移成本很低配置文件基本可以复用。2.3 兼容 OpenAI 协议一个被低估的战略选择litellm 的接口刻意向 OpenAI 的 API 格式靠拢包括请求体的messages结构、choices[0].message.content的返回路径、流式的 SSE 格式。这个选择看似普通实则是它能快速铺开的关键。因为现在市面上绝大多数客户端工具、框架、前端组件都是按 OpenAI 协议写的。只要你的服务兼容这套协议这些工具不用改一行代码就能接进来。比如很多聊天界面、很多 Agent 框架配置里填个 base_url 和 api_key 就能用。litellm 代理正是利用这一点把自己伪装成一个“OpenAI 服务”从而获得了整个生态的兼容性。提示兼容不等于完全一致。部分厂商特有的能力比如某些多模态参数、特殊的工具调用格式在标准化过程中可能会有损。用到冷门特性时建议先查文档确认是否被支持。2.4 配置驱动把易变的部分挪出代码litellm 代理的核心是一份 YAML 配置文件里面定义模型列表、路由策略、密钥来源、限流规则。这个设计背后的逻辑是模型选型和密钥是易变的业务逻辑是相对稳定的。把易变的部分放进配置就能在不改代码、不重新部署的前提下调整线上行为。我见过太多项目把模型名硬编码在代码里结果换个模型要走完整的测试和发布流程。用配置驱动之后运营甚至可以在后台改配置来切换模型研发只需要保证配置格式正确。这种职责分离在规模化之后收益非常明显。3. 核心细节解析与实操要点从安装到第一次调用3.1 安装与最小可用示例安装本身没什么门槛pip install litellm如果你要用代理功能建议装带 proxy 的扩展pip install litellm[proxy]最小调用示例我这里用环境变量管理密钥这是最基本的安全习惯import os from litellm import completion os.environ[OPENAI_API_KEY] your-key-here response completion( modelgpt-4o-mini, messages[{role: user, content: 用一句话解释什么是向量数据库}] ) print(response.choices[0].message.content)注意这里的completion函数签名和 OpenAI 官方 SDK 几乎一模一样。这就是前面说的“兼容协议”带来的好处——你已有的调用代码改个 import 和 model 名就能跑。3.2 密钥管理的三种姿势与取舍密钥怎么放是个看似小、实则影响安全的大事。我总结三种常见做法方式适用场景优点风险环境变量本地开发、单机部署简单、不落盘进程内可读日志可能泄露配置文件引用环境变量代理模式集中管理、可版本化配置文件本身要保护好密钥管理服务生产环境、多团队审计、轮换方便接入成本高litellm 的配置文件支持用os.environ/VAR_NAME这种写法引用环境变量这样配置文件里不出现明文密钥可以放心提交到代码仓库。这是我强烈推荐的做法model_list: - model_name: gpt-4o-mini litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY注意千万不要把明文密钥写进 YAML 然后提交到 Git。哪怕后来删了历史记录里还在。用环境变量引用是底线操作。3.3 模型命名规则前缀决定路由litellm 的模型命名有一套约定理解它能少走很多弯路。基本格式是provider/model-name。比如openai/gpt-4o走 OpenAIanthropic/claude-3-5-sonnet-20241022走 Anthropicgemini/gemini-1.5-pro走 Googleollama/llama3走本地 Ollamaazure/your-deployment-name走 Azure有些常用模型可以省略前缀litellm 会自动识别但我建议始终写全前缀因为自动识别偶尔会猜错尤其是模型名有歧义的时候。显式声明路由出问题时排查方向也清晰。对于自建或兼容 OpenAI 协议的服务可以用openai/前缀配合自定义api_base- model_name: my-local-model litellm_params: model: openai/my-model api_base: http://localhost:8000/v1 api_key: dummy这里的api_key即使服务不需要鉴权很多客户端也要求非空填个占位符即可。3.4 参数透传与覆盖fallback 与 retry 的配置逻辑生产环境里模型服务偶尔抽风是常态。litellm 提供了两层容错重试retry和降级fallback。重试是同一个模型失败后再试几次适合网络抖动这类瞬时问题。降级是主模型彻底不可用时切到备用模型。配置大概长这样litellm_settings: num_retries: 3 request_timeout: 30 fallbacks: [{gpt-4o: [claude-3-5-sonnet, gemini-1.5-pro]}]这段配置的意思是gpt-4o 调用失败时依次尝试 claude 和 gemini。注意 fallback 是有序列表会按顺序尝试所以要把性价比高、稳定性好的放前面。这里有个容易踩的坑不同模型的参数不通用。比如你给 gpt-4o 传了temperature0.7降级到某个不支持该参数的模型时可能报错。litellm 会尽量做参数映射但不是万能的。稳妥做法是只用最通用的参数messages、temperature、max_tokens把厂商特有参数放到单独的调用里。4. 实操过程与核心环节实现搭一个可用的模型网关4.1 配置文件从零写起下面这份配置是我在实际项目里用过的精简版涵盖了多模型、密钥引用、重试和降级model_list: - model_name: fast litellm_params: model: openai/gpt-4o-mini api_key: os.environ/OPENAI_API_KEY - model_name: smart litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY - model_name: cheap litellm_params: model: gemini/gemini-1.5-flash api_key: os.environ/GEMINI_API_KEY litellm_settings: num_retries: 2 request_timeout: 60 fallbacks: [{smart: [fast]}, {fast: [cheap]}] general_settings: master_key: os.environ/LITELLM_MASTER_KEY这里我用了三个语义化的别名fast、smart、cheap。业务代码里只认这三个名字具体背后是哪家模型由配置决定。这样切换模型时业务代码零改动。这是我个人最推荐的组织方式——用业务语义命名而不是用厂商模型名。master_key是代理的管理密钥用来访问管理接口、查看用量等务必设置且不要外泄。4.2 启动代理服务与验证配置写好之后启动命令很简单litellm --config config.yaml --port 4000启动后它会监听 4000 端口对外提供 OpenAI 兼容接口。验证一下curl http://localhost:4000/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $LITELLM_MASTER_KEY \ -d { model: fast, messages: [{role: user, content: 你好}] }如果返回结构里有choices说明链路通了。这时候你项目里所有原本调 OpenAI 的地方只要把 base_url 改成http://localhost:4000api_key 改成你的 master_key 或虚拟 key就能无缝切换。4.3 虚拟 Key 与预算控制多团队协作的关键代理模式最实用的功能之一是虚拟 Key。你可以给每个团队、每个应用发一个独立的 key每个 key 可以设置预算上限、可用模型范围、限流阈值。这样某个应用失控狂调模型时不会拖垮整个账单。创建虚拟 key 一般通过管理接口curl http://localhost:4000/key/generate \ -H Authorization: Bearer $LITELLM_MASTER_KEY \ -H Content-Type: application/json \ -d { models: [fast, cheap], max_budget: 50, budget_duration: 30d }这段配置的意思是这个 key 只能用 fast 和 cheap 两个模型30 天内最多花 50 美元。返回的 key 发给对应团队即可。预算耗尽后该 key 的请求会被拒绝但其他 key 不受影响。提示预算统计依赖 litellm 内置的价格表。如果用的是自建模型或价格表里没有的模型花费可能统计为 0。这种情况需要手动配置模型价格否则预算控制形同虚设。4.4 流式输出与超时处理流式输出在聊天类应用里几乎是刚需。litellm 支持streamTrue返回的是一个生成器逐块吐出内容response completion( modelfast, messages[{role: user, content: 写一首短诗}], streamTrue ) for chunk in response: delta chunk.choices[0].delta.content if delta: print(delta, end, flushTrue)这里有个实操细节流式场景下的超时和重试逻辑要特别小心。因为流已经开始吐数据了中途失败再重试用户会看到重复内容。我的做法是流式请求把num_retries设小一点比如 1并且在前端做去重和断流提示。非流式请求可以放心用较大的重试次数。另外request_timeout对流式请求的含义是“首字节超时”还是“整体超时”不同版本行为可能有差异建议实测确认。我一般设 60 秒长文本生成场景适当放宽。5. 常见问题与排查技巧实录5.1 报错速查表下面这张表是我踩坑之后整理的覆盖了八成以上的常见报错报错关键词可能原因排查方向AuthenticationError密钥错误或未设置检查环境变量是否被正确读取NotFoundError / model not found模型名拼写错误或前缀缺失确认 provider 前缀和模型名RateLimitError触发厂商限流加退避重试或换 keyContextWindowExceeded输入超长截断历史或换长上下文模型Timeout网络或模型响应慢调大 timeout检查网络InvalidRequestError参数不被支持检查是否传了厂商特有参数排查的第一步永远是打开详细日志。litellm 支持设置litellm.set_verbose True或者在代理启动时加--detailed_debug能看到完整的请求体和响应体。很多问题看一眼原始请求就明白了。5.2 参数不兼容的隐蔽坑前面提过参数透传的问题这里展开说。不同厂商对同一个概念可能用不同参数名。比如控制随机性OpenAI 用temperature和top_p有些厂商只支持其中一个。再比如max_tokens有的厂商叫max_output_tokens。litellm 会做一层映射但映射表不可能覆盖所有情况。我的经验是跨模型调用时只用最基础的参数。如果某个场景必须用厂商特有参数就单独为那个模型写一条调用路径不要指望统一接口能处理一切。这不是 litellm 的缺陷而是“统一抽象”这件事本身的边界——抽象层越通用对特殊能力的支持就越有限。5.3 成本统计不准怎么办前面提到价格表的问题。litellm 内置了一张主流模型的价格表但模型更新很快价格也会变。如果发现统计的花费明显偏低或为 0大概率是模型不在表里。解决办法是在配置里手动指定价格- model_name: my-model litellm_params: model: openai/my-model input_cost_per_token: 0.000001 output_cost_per_token: 0.000002单位是“每 token 的美元价格”。填的时候注意区分输入和输出输出通常更贵。这个数字填错会导致预算控制失真建议对着厂商官网的价格页仔细核对。5.4 代理服务的性能与部署建议代理模式虽然方便但它多了一跳网络请求会带来额外延迟。实测下来同机房内这一跳通常在几毫秒到十几毫秒相比模型本身几百毫秒到几秒的响应时间可以忽略。但如果代理和应用跨地域部署延迟就会明显。我的部署建议是代理服务和应用服务部署在同一区域减少网络往返。代理本身是无状态的除了用量统计可以水平扩展前面挂个负载均衡即可。用量统计如果用了数据库后端要注意并发写入的压力高流量场景建议用独立的数据库实例。注意代理服务持有所有厂商的密钥是安全敏感点。务必限制它的网络访问范围不要直接暴露在公网管理接口尤其要保护好。6. 我踩过的几个真实坑与经验总结第一个坑是环境变量读取时机。litellm 在导入时或首次调用时会读取环境变量如果你在 import 之后才设置os.environ可能不生效。我当时的做法是把密钥设置放在最前面或者干脆用.env文件配合加载库在程序入口统一加载。这个坑很隐蔽因为报错信息只说你没鉴权不会告诉你是因为读取时机不对。第二个坑是fallback 链的循环。有次我配置了 A 降级到 B、B 又降级到 A结果两个模型都挂的时候请求在两个之间来回横跳直到超时。后来我改成单向链smart 到 fast 到 cheap绝不回头。降级链一定要是有向无环的这是配置时的硬性纪律。第三个坑是流式响应的错误处理。非流式请求失败会抛异常好处理流式请求可能已经吐了一半数据才失败这时候前端如果直接展示用户会看到半截内容。我的处理方式是在流式回调里捕获异常一旦出错就发送一个明确的结束标记前端收到后提示“生成中断请重试”而不是让用户对着半截内容发呆。第四个坑是模型别名的命名混乱。早期我直接用厂商模型名做别名结果换模型时业务代码里到处是硬编码的模型名等于白折腾。后来统一改成业务语义别名才真正享受到配置驱动的红利。这个教训值得反复强调别名要表达用途不要表达实现。最后分享一个实用技巧litellm 支持在请求头里传一些元数据用于日志和用量归因。比如你可以给每个请求带上调用方的标识这样在查看用量报表时能清楚知道是哪个业务线在消耗预算。这个功能在多团队共用网关时特别有用建议一开始就规划好元数据规范别等到账单爆炸了才想起来追溯。这套东西我从最初的脚本调用一路用到现在的多团队共享网关中间迭代了好几轮配置。核心体会就一句统一接口的价值不在于省几行代码而在于把模型选型这件事从代码里解耦出来变成可以随时调整的配置。想清楚这一点很多设计取舍就顺理成章了。

相关新闻

心智词生成的框架效应约束:三看标准与品类锚点检测

心智词生成的框架效应约束:三看标准与品类锚点检测

一、问题定义 LLM生成品类词时存在两个系统性倾向:一是生成“准确但记不住”的长词(“0蔗糖低温烘焙燕麦片”式);二是生成听着很美、实则空洞的内部思维词(“新中式”“智能家居”式)。两者都会导致品类词模…

2026/10/14 9:31:13 阅读更多 →
litellm 实战:用统一接口调用上百家大模型,告别适配地狱

litellm 实战:用统一接口调用上百家大模型,告别适配地狱

1. 从一个“模型调用地狱”说起:litellm 到底解决了什么问题如果你最近半年在折腾大模型应用,大概率会遇到这样一个场景:项目一开始只想接一家模型服务,代码写得干干净净,一个 SDK 走天下。结果产品经理跑过来说&#…

2026/10/14 9:30:12 阅读更多 →
从工具到技能:Agent技能化设计与编排实战指南

从工具到技能:Agent技能化设计与编排实战指南

从标题里看到 agent-skills 的第一反应,这不是一个普通的小工具,而是一类越来越常见的系统设计思路:把大模型的能力从“对话”拆成一组可被复用、调度、管理的“技能”。如果你正在做智能体(Agent)相关的项目&#xff…

2026/10/14 9:30:12 阅读更多 →

最新新闻

Hive 源码导读(八):MySQL 只是慢了,Metastore 为什么会 OOM?

Hive 源码导读(八):MySQL 只是慢了,Metastore 为什么会 OOM?

上一篇,我们沿着订单日报的执行阶段,讨论了“SQL 一直运行中,到底该去哪里查”。其中有一站很容易被忽略:访问 Metastore。 这次接着往下走,聊一个实际遇到的案例。 最近,线上 Hive Metastore 发生过一次…

2026/10/14 10:33:01 阅读更多 →
自然语言优化求解:大模型+中间IR+PuLP链路实践

自然语言优化求解:大模型+中间IR+PuLP链路实践

说句实话,把“人话”变成一个能求最优解的数学模型,这个方向我琢磨了很久。最开始纯粹是业务里的痛点:每次排生产计划都要拉着懂运筹的人,把口头需求一点点翻译成约束和目标函数,稍不注意就漏条件。后来我决定自己动手…

2026/10/14 10:33:01 阅读更多 →
ANSYS 2025 R1电磁仿真套件安装全攻略:从环境准备到许可证配置

ANSYS 2025 R1电磁仿真套件安装全攻略:从环境准备到许可证配置

搞电磁仿真的人,最近应该都注意到 ANSYS 2025 R1 这个版本放出来了。作为常年跟 HFSS、Maxwell、Q3D 打交道的工程师,每次大版本更新,我关心的无非就三件事:好不好装、稳不稳、新功能值不值得升级。这次 2025 R1 在电磁电子仿真这…

2026/10/14 10:33:01 阅读更多 →
手写NumPy逻辑回归:从数学公式到可调试代码

手写NumPy逻辑回归:从数学公式到可调试代码

1. 项目概述:为什么一个“基于numpy的逻辑回归”值得从头手写一遍?在某高校数据科学导论课的期末项目里,我带过一批零基础转行的同学,他们第一次接触机器学习时,几乎所有人都卡在同一个地方:调用sklearn.Lo…

2026/10/14 10:33:01 阅读更多 →
架构流程图设计指南:从核心要素到实操步骤的完整解析

架构流程图设计指南:从核心要素到实操步骤的完整解析

1. 架构流程图到底在画什么:从一张图看懂系统骨架很多人第一次接触“架构流程图”这个词,脑子里浮现的是一堆方框和箭头,觉得无非就是把系统画出来而已。但真正动手画过几套系统的人都知道,一张好的架构流程图,价值不亚…

2026/10/14 10:33:01 阅读更多 →
Unity UI卡顿排查:Canvas重建与脏标记优化实战

Unity UI卡顿排查:Canvas重建与脏标记优化实战

做 Unity 性能优化的人,大概率都遇到过这种场景:主逻辑 CPU 占用看着很正常,帧率也稳,可一打开背包或者商店界面,帧数立刻往下掉。打开 Profiler 一看,UI 模块标红,Canvas 相关函数的耗时占了很…

2026/10/14 10:31:57 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

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

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

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

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

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

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

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

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

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

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 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/13 22:01:27 阅读更多 →
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/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练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/14 7:53:34 阅读更多 →