LangChain内置中间件实战:构建稳定客服Agent的五个关键组件

聊到Agent开发,很多人第一反应是“让模型自己决定”,可真正写起业务代码时才明白:模型只负责“想”,真正让Agent稳定干活的是围绕在它外面的一圈管路和阀门。这些管路,在LangChain里就是内置中间件。这篇不讲高深理论,直接把这些为Agent准备的内置中间件挨个捋清楚,从提示模板到输出解析器,从记忆到工具封装,再通过一个能直接照抄的客服Agent示例,让你知道每层组件在什么位置、解决什么问题、有哪些实战中的坑。适合刚接触LangChain不久、想把Agent真正用起来的开发者。

1. 先搞清楚“LangChain内置中间件”到底是什么

1.1 Agent的执行链路与中间件定位

先看一个最常见的误解:有人以为Agent就是“模型 + 提示词”,跑起来会自动干活。实际上,一个没有中间件的裸Agent是非常脆弱的:你丢给它一句话,它可能答非所问,可能编造工具结果,也可能在同一个步骤里反复横跳。问题不在于模型,而是缺少了夹在“模型”和“业务”之间的那层处理逻辑。

在LangChain里,Agent的执行链路大体是这样的:先接收用户输入,然后从Prompt模板里组装出完整指令,交给模型推理;模型决定调用哪个工具,LangChain去执行工具并把结果喂回模型;模型再基于观察继续推理,直到给出最终答案。这个链路里,每一步的前后都需要有组件做“翻译”和“状态管理”,而这些组件就是我们说的中间件。

把LangChain内置中间件想象成一家餐厅的后厨流程:前台接单(用户输入),菜单设计(Prompt模板),传菜员(工具执行),品控员(输出解析),账本记录(记忆),监控摄像头(回调)。任何一个环节缺失,整个服务都会混乱。

1.2 内置中间件的完整分类

LangChain内置的中间件种类并不算少,但如果按功能划分,基本可以归纳为五类:

中间件核心职责常见具体组件
提示模板管理给模型的指令,注入上下文和变量PromptTemplate、ChatPromptTemplate
输出解析把模型的自然语言回复转成结构化数据PydanticOutputParser、StructuredOutputParser
记忆管理保存历史对话和摘要,维护多轮状态ConversationBufferMemory、ConversationSummaryBufferMemory
工具封装把业务函数变成模型可调用的“能力接口”Tool、@tool装饰器、Toolkits
回调与观测记录Agent每步动作,方便调试和追踪BaseCallbackHandler、ConsoleCallbackHandler

有人会把“模型本身”也算成中间件,其实不太准确。模型是Agent的核心动力源,中间件是包裹在外的支持系统。真正有价值的工程化工作,也恰恰集中在这五类中间件上。

2. 逐类拆解五个核心内置中间件

2.1 提示模板中间件:不要每次手写系统提示词

刚开始写Agent时,我喜欢在代码里直接拼字符串,比如“system: 你是一个客服,用户问题是:xxx”。这种写法在Demo里没问题,但一旦做多轮对话、多场景并发,你会发现代码又乱又难维护。

PromptTemplate的价值在于把提示词变成可复用的模板变量体系。比如在LangChain里,用ChatPromptTemplate可以清晰地区分系统消息、人类消息、AI消息:

from langchain.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "你是{shop_name}的智能客服,可以查询订单、物流、退款信息。回答务必简洁。"), ("human", "{input}") ])

注意{shop_name}和{input}都是变量。实际调用时传入prompt.invoke({"shop_name": "某店铺", "input": "我要查订单"}),模板会把消息组装好。好处很明显:不同店铺、不同业务线可以复用同一个模板结构,只需要换参数。

更进阶的用法是给模板加少数示例(Few-Shot)。比如一个意图识别Agent可以在模板里先给一个“用户问快递 -> 查询物流”的示例,再让模型做新分类。这种中间件直接影响了模型后续表现,属于性价比最高的优化点。

这里有一个实战心得:模板里的System指令不要写得过长。很多人爱把规则一条条堆到几百字,发现Agent还是爱答非所问。其实跟写接口文档一样,指令要精简、分块、给优先级。把最关键的约束放前头,比一股脑塞给模型更有效。

2.2 输出解析中间件:把模型的“话”变成程序能用的“数据”

模型回复本质上是一段文本,程序不能直接拿来做分支判断。这时输出解析中间件就派上用场了。LangChain内置了多种解析器,最常用的是PydanticOutputParser:定义好目标数据结构,解析器会引导模型按JSON格式输出,再校验成我们需要的对象。

看一个实际例子,假设客服Agent需要先判断用户意图:

from pydantic import BaseModel, Field from langchain.output_parsers import PydanticOutputParser class IntentResult(BaseModel): intent: str = Field(description="用户意图,取值只能是:订单查询、物流查询、退款申请、其他") order_id: str | None = Field(default=None, description="用户提到的订单号,没有则为null") confidence: float = Field(description="置信度,0到1之间") parser = PydanticOutputParser(pydantic_object=IntentResult)

使用的时候,把parser.get_format_instructions()拼进Prompt模板,让模型按照指定格式返回。拿到回复后调用parser.parse(llm_output),就能得到IntentResult对象。如果模型输出不合法,解析器会抛出异常,我们可以捕获后重新让模型修正一次。

这背后有个设计逻辑:输出解析不是“事后修正”,而是“过程约束”。只要Prompt里明确说了“必须返回JSON,字段包括xxx”,模型生成非法结果的概率会大幅降低。所以别等着出错再补异常,先把这个中间件用起来,会有立竿见影的效果。

2.3 记忆中间件:让Agent记住上下文

没有记忆的Agent每次对话都是“初次见面”。你可以把上一轮内容拼进Prompt,但很快会发现Token占用越来越大,而且真正重要的历史信息被淹没在一堆废话里。

LangChain内置了几种记忆中间件,我按使用场景说:

  • ConversationBufferMemory:直接缓存所有历史消息,适合短对话,缺点是会快速增长。
  • ConversationSummaryMemory:用模型定期总结历史内容,只保留摘要,省Token,适合长会话。
  • ConversationSummaryBufferMemory:折中方案,既保留最近的原始消息,又对更早的内容做摘要。

下面这段代码是典型的“缓冲+摘要”用法:

from langchain.memory import ConversationSummaryBufferMemory from langchain.chat_models import ChatOpenAI memory = ConversationSummaryBufferMemory( llm=ChatOpenAI(model="gpt-4o"), max_token_limit=6000, return_messages=True )

大概逻辑是:当全部消息的Token数量超过max_token_limit时,记忆组件会调用llm对最老的历史做摘要,再用摘要替代原始内容。这样既能保住近几轮的细节,又不会让上下文无限膨胀。

使用记忆中间件时要注意,Agent场景下记忆不只是“存历史”,还要把记忆和工具调用结果分开。建议给记忆打标签或者分区存储,避免把工具返回的大段JSON混入对话历史,导致模型判断混乱。

2.4 工具中间件:把外部能力封装给模型调用

工具(Tools)是Agent区别于普通聊天模型的关键。LangChain内置了大量工具,比如各类搜索接口、数据库查询接口等,但实际项目里更多是自己写函数然后包装成工具。而这恰恰是最能体现“中间件”价值的地方。

用@tool装饰器封装一个订单状态查询函数:

from langchain.tools import tool @tool def get_order_status(order_id: str) -> str: """根据订单号查询订单的当前状态。返回发货时间、物流节点等信息。""" # 这里写真实的业务查询逻辑 return f"订单{order_id}已发货,当前在运输中,预计明天送达。"

装饰器会自动读取函数的参数列表、类型、文档字符串,生成一个结构化的工具描述。模型拿到这个描述就知道:“哦,有一个get_order_status函数,参数是字符串类型的order_id,可以查订单状态。”然后在需要时发出调用指令,LangChain执行工具并把结果返回给模型。

实战里的一个关键点是文档字符串不能乱写。工具描述是模型选择工具的主要依据,描述含糊会导致模型根本不知道该调用谁。建议每个工具描述都明确写明“什么时候用”“参数是什么含义”“返回什么结果”。

2.5 回调与观测中间件:偷看Agent的思考过程

中间件里最容易被忽略的是回调。调试Agent时,如果你只看到最终输出,一旦出错根本不知道是哪一步出了问题。回调机制相当于给Agent装了一整套监控仪表。

LangChain内置了BaseCallbackHandler,你可以在不同阶段挂上处理函数:

from langchain.callbacks.base import BaseCallbackHandler class DebugHandler(BaseCallbackHandler): def on_llm_start(self, serialized, prompts, **kwargs): print("模型开始推理,提示词:", prompts) def on_tool_start(self, serialized, input_str, **kwargs): print("调用工具,输入:", input_str) def on_tool_end(self, output, **kwargs): print("工具返回:", output)

把DebugHandler实例传给AgentExecutor的callbacks参数,就能实时打印每一步动作。我第一次排查Agent循环问题时就是靠这个定位到:模型反复调用同一个查询工具,因为没有把“结果已拿到”写进上下文。看到日志后,问题一分钟就定位了。

3. 手把手实现一个带全套中间件的客服Agent

3.1 场景设计与选型

为了让你直观看明白这些中间件怎么协同工作,我做一个智能客服Agent。场景是这样的:用户问某个订单的状态,Agent能调用订单查询工具,能记住用户之前聊过的内容,并且最终输出格式化的意图和答复。

我的中间件选型如下:

  • 提示模板:ChatPromptTemplate,负责系统指令和变量注入。
  • 输出解析:PydanticOutputParser,把最终回答解析成AgentReply对象。
  • 记忆:ConversationSummaryBufferMemory,既保留近期对话,又压缩更早历史。
  • 工具:@tool装饰器包装一个查询函数。
  • 回调:自定义Handler,打印运行日志。

这种组合方式在中小型业务系统里非常通用,每一步都是LangChain内置能力,不需要自己造轮子。

3.2 代码实现与逐段讲解

先准备好所有依赖和初始化:

from langchain.agents import AgentExecutor, create_openai_functions_agent from langchain.chat_models import ChatOpenAI from langchain.memory import ConversationSummaryBufferMemory from langchain.output_parsers import PydanticOutputParser from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder from langchain.tools import tool from pydantic import BaseModel, Field class AgentReply(BaseModel): answer: str = Field(description="面向用户的最终回答") order_id: str | None = Field(default=None, description="本次涉及的订单号,没有则为null") parser = PydanticOutputParser(pydantic_object=AgentReply)

接着定义Prompt模板。这里特别加入了MessagesPlaceholder,用于给AgentExecutor插入历史对话和中间步骤:

prompt = ChatPromptTemplate.from_messages([ ("system", "你是某店铺的客服Agent。请根据工具返回结果回答用户问题。" "最终回答必须按以下格式返回:\n{format_instructions}"), MessagesPlaceholder(variable_name="chat_history"), ("human", "{input}"), MessagesPlaceholder(variable_name="agent_scratchpad"), ]).partial(format_instructions=parser.get_format_instructions())

agent_scratchpad是LangChain内部约定给Agent保存“想法、动作、观察”的临时空间。这里不建议随意改名。

然后定义工具和记忆:

@tool def get_order_status(order_id: str) -> str: """根据订单号查询订单状态,包含发货和物流信息。""" # 模拟查询 return f"订单{order_id}状态为运输中,预计明天送达" memory = ConversationSummaryBufferMemory( llm=ChatOpenAI(model="gpt-4o"), max_token_limit=6000, return_messages=True )

最后组装Agent并执行:

llm = ChatOpenAI(model="gpt-4o", temperature=0) agent = create_openai_functions_agent( llm=llm, tools=[get_order_status], prompt=prompt, ) executor = AgentExecutor( agent=agent, tools=[get_order_status], memory=memory, max_iterations=3, handle_parsing_errors=True, verbose=False, )

这里重点说明AgentExecutor的几个关键参数。max_iterations限制最大推理循环次数,防止模型反复思考不收敛。handle_parsing_errors=True表示当模型输出无法解析时,允许把异常信息返回给模型,让模型自我修正。这是个很实用的容错设计。

3.3 运行效果与分析

顺序执行两个问题,看看记忆中间件是否生效:

response = executor.invoke({"input": "帮我查一下订单A1001的状态"}) print(response["output"]) response2 = executor.invoke({"input": "这个订单什么时候能到?"}) print(response2["output"])

第一轮调用工具返回运输中,模型生成答复。第二轮虽然用户没提订单号,但记忆中间件保留了历史记录里的“订单A1001”,模型能自动关联上文,回复“预计明天送达”。如果没有记忆,这轮铁定要用户再补一次订单号。

这整套代码里,提示模板负责约束模型的输出格式,输出解析器把回复转成结构化对象,工具封装了业务能力,记忆串联多轮上下文。五个中间件各司其职,缺哪一个,整个闭环都有断点。

4. 常见问题与排查技巧实录

4.1 输出解析频繁失败的三种原因

我在实际项目中遇到过不少解析错误,绝大多数是这三类:

第一,Prompt里没有明确贴出格式要求。很多人只给了parser.get_format_instructions(),却没跟模型强调“必须严格遵守,不能输出额外文字”。建议在System指令里加一句“只能输出JSON,不要返回解释”。

第二,字段描述写得模糊。比如把order_id描述成“订单信息”,模型可能输出对象而不是字符串。字段描述要具体到格式和取值范围。

第三,工具返回内容干扰了模型。如果工具返回一长串格式不规则的文本,模型在组织回答时容易跑偏。解决办法是在工具描述里声明“返回纯文本,不需要客户看到内部格式”。

4.2 记忆混乱和Token超限怎么办

记忆组件不是存得越多越好。ConversationSummaryBufferMemory虽然会自动压缩,但压缩时依赖LLM做摘要,如果摘要模型的上下文长度不够,反而会丢关键信息。

排查技巧是查看记忆中间件最终注入到Prompt的消息内容。我一般会在回调Handler里打印chat_history,看注入的是原始消息还是摘要,判断是否出了问题。另外,如果业务允许,最简单的策略是给Agent直接限定单轮对话轮数,超过后就提示用户重新开始,这对很多客服场景完全够用。

4.3 工具调用循环和死循环怎么破

一个经典故障场景:模型调用查询工具后,拿到结果却说“还需要进一步查询”,于是又调用一次,如此反复直到超时。这通常是因为模型没有从工具结果中提取到关键信息,或者Prompt没告诉它“拿到结果后就生成最终回答”。

解决思路有三个层面:降低max_iterations,这是兜底;在System指令里明确“工具结果包含所需信息时,必须停止推理”;检查工具返回内容是否过于冗长,模型可能在其中找不到重点。我曾在代码里让工具只返回“已发货”而不是大段物流轨迹,循环问题立刻少了很多。

4.4 额外建议与最佳实践

用内置中间件时,有几条比较通用的经验:

场景建议
多业务线复用把Prompt模板、工具列表固化成配置,不同业务传不同参数
生产环境上线一定要接入回调日志,记录每次LLM调用和工具调用的耗时、结果
解析结果不稳定对关键字段写校验逻辑,不满足条件就强制走人工兜底
记忆数据敏感不要把所有对话都塞进记忆,先做脱敏和过滤

这些建议都是从踩坑里总结出来的,尤其第二条,没有日志的Agent项目出了问题只能靠猜。

最后再分享一个小技巧

如果你刚开始搭Agent,先别急着加知识库、加复杂路由,老老实实用LangChain内置的这套中间件把“提示模板 + 输出解析 + 记忆 + 工具 + 回调”的闭环跑通。等闭环稳定了,再往里加场景。我个人在实际调试中还有一个习惯:任何一次Agent行为异常,先开Handler看日志,再精简Prompt,最后才怀疑模型能力。多数问题其实都出在中间件配置和Prompt设计上,模型本身比我们想象的更靠谱。这套经验,在我经历的好几个项目中都被反复验证过。

相关新闻

Fastjson 1.2.24反序列化利用链深度解析:JdbcRowSetImpl与JNDI注入

Fastjson 1.2.24反序列化利用链深度解析:JdbcRowSetImpl与JNDI注入

1. 项目概述:为什么这个利用链至今仍被反复提及“fastjson 1.2.24 反序列化 JdbcRowSetImpl 利用链”——这串字符组合,对做过Java后端安全研究、渗透测试或WAF规则开发的人来说,几乎刻在肌肉记忆里。它不是某个冷门漏洞的代号,而…

2026/10/20 3:47:13 阅读更多 →
大模型落地核心:上下文工程实战指南

大模型落地核心:上下文工程实战指南

【必收藏】AI落地训练营:详解大模型核心技巧——上下文工程早期做大模型落地的时候,我踩过一个特别典型的坑:明明模型能力很强,参数也调了,提示词也写得花团锦簇,但一上真实业务场景,效果立刻拉…

2026/10/20 3:47:13 阅读更多 →
Nginx安全头配置实战:八个核心响应头加固Web安全

Nginx安全头配置实战:八个核心响应头加固Web安全

1. 为什么Nginx安全头值得专门写一篇做了这么多年Web服务端,我见过太多把Nginx配置当成"转发请求"工具就完事儿的场景。反向代理、负载均衡、静态资源服务都配置得明明白白,但HTTP响应头这块儿,几乎是被遗忘的角落。直到某次给客户…

2026/10/20 3:46:12 阅读更多 →

最新新闻

Agent与Milvus联手:为AI助手构建动态记忆,突破静态知识库天花板

Agent与Milvus联手:为AI助手构建动态记忆,突破静态知识库天花板

被静态知识库折腾过的人,一定懂那种感觉:文档喂了几百份,问答效果还是“看似会答,实则空洞”。问文档里有的东西,答得还行;一旦用户问题换了个说法,或者需要结合上一次聊天的上下文,…

2026/10/20 4:42:47 阅读更多 →
交换机MAC地址Hash冲突:原理、排查与治理

交换机MAC地址Hash冲突:原理、排查与治理

你大概率遇到过一个这样的场景:核心交换机CPU不高、带宽没打满,环路排查做了两遍也没发现异常,但业务侧的反馈就是时断时续,视频会议卡成PPT,终端ping网关的丢包率像过山车。最后技术大佬甩过来一句话:“查…

2026/10/20 4:42:47 阅读更多 →
ADS1256高精度驱动实战:STM32F407上SPI时序、校准与定点换算全解析

ADS1256高精度驱动实战:STM32F407上SPI时序、校准与定点换算全解析

简介:本资源是一套基于STM32F407微控制器驱动ADS1256高精度24位Σ-Δ型ADC的完整嵌入式开发代码包,面向嵌入式初学者、传感器数据采集系统开发者及工业测量类项目实践者,解决高分辨率模拟信号采集与MCU可靠通信的核心难题。压缩包共289个文件…

2026/10/20 4:42:47 阅读更多 →
Unity C#反射实战:动态加载、配置驱动与热更新

Unity C#反射实战:动态加载、配置驱动与热更新

1. 为什么反射在Unity游戏开发里不是“炫技工具”,而是解决实际问题的扳手“Unity C# 游戏开发 反射 Reflection 案例讲解”——这个标题里藏着一个被很多新手误读的真相:反射从来不是为了写得“看起来高级”,而是当你在Unity编辑器里拖拽了二…

2026/10/20 4:42:47 阅读更多 →
Grok Bot:面向企业采购的决策代理系统

Grok Bot:面向企业采购的决策代理系统

1. Grok Bot 不是“购物插件”,而是一套可定制的比价与下单决策代理系统很多人看到“Grok Bot 可帮你比价并下单购物”这个标题,第一反应是:又一个浏览器插件?点一下自动跳转京东淘宝比价格?其实完全不是。我接触过三个…

2026/10/20 4:42:47 阅读更多 →
Kafka消息不丢失的完整保障机制:生产者、Broker与消费者协同配置指南

Kafka消息不丢失的完整保障机制:生产者、Broker与消费者协同配置指南

1. 先说结论:Kafka的“不丢失”是有前提的Kafka声称能保证消息不丢失,但这个承诺不是无条件的。很多初学者上来就以为“用了Kafka就万事大吉”,结果线上丢数据之后一脸懵。实际上,Kafka的消息可靠性是一个“端到端”问题&#xff…

2026/10/20 4:41:47 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

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

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

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

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

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

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 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/19 2:54:50 阅读更多 →
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/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练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/18 12:16:16 阅读更多 →