news 2026/9/5 8:29:40

函数调用实战:从本地部署到多轮调用与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
函数调用实战:从本地部署到多轮调用与避坑指南

先说一个我自己的直观感受:大模型真正开始“值钱”,不是从它能聊天开始的,而是从它能调用工具、完成任务开始的。你把模型当成一个只会说话的顾问,它顶多帮你润色文案;但你教会它调接口、发请求、查数据库、操作业务系统,它才从“聊天窗口”里走出来,变成了一个能帮你干活的数字员工。这个转折点,靠的就是 Function Calling(函数调用)。

这篇实战指南,我不打算给你堆概念。我直接带你过一遍 Function Calling 从原理到落地的全过程,包括本地模型怎么跑、OpenAI 风格接口怎么调、JSON Schema 怎么定义参数、多轮调用怎么处理状态、以及我在真实项目中踩过的那些坑。无论你是刚接触大模型应用开发,还是已经做了几个 Demo 但觉得不够“扎实”,这篇文章都能给你一条可以照着走的路线。

1. 先搞清楚 Function Calling 到底在解决什么问题

1.1 没有 Function Calling 的时候,大模型有多“笨”

很多人第一次用大模型 API 做应用时,都会遇到一个尴尬场景:你问它“北京今天天气怎么样”,它回答得头头是道,但实际天气数据是它编的。你问它“帮我查一下订单号 20250101 的物流状态”,它要么说“我无法访问外部系统”,要么开始一本正经地编造物流轨迹。

这背后的原因很简单:大模型本身是一个“文本生成器”,它没有主动查询数据库、调用 API、操作文件的能力。它的知识截止到训练数据那一刻,之后发生的事情它一概不知。所以,你让它“查一下”“算一下”“提交一下”,它只能靠概率去编一个看起来合理的答案。

没有 Function Calling 之前,开发者想解决这个问题,只能用“提示词硬刚”的方式:在 Prompt 里告诉模型“如果用户想查天气,你就输出【查天气】北京”,然后自己在代码里解析这段文字,再调天气接口,最后把结果拼接回去。这个方案能跑,但极其脆弱。模型稍微换个措辞,解析就崩了;多几个工具,Prompt 就臃肿得没法维护;一旦模型在中间步骤输出一些“废话”,你的解析逻辑就要跟着崩。

1.2 Function Calling 把“意图识别”和“参数提取”从提示词里解放出来

Function Calling 的核心思路,不是让模型自己决定“要不要调用工具”,而是你提前把工具的描述、参数结构告诉模型,模型在回答时如果觉得需要调用工具,就输出一个结构化的“调用请求”。这个请求里包含工具名称和参数,你的代码拿到这个请求后自己去执行真实函数,再把结果返回给模型,模型最后组织成自然语言回答用户。

换句话说,Function Calling 做的事,是把“意图识别”和“参数提取”这两个关键步骤从“纯文本约定”变成了“结构化协议”。模型不再需要靠输出特定文字来暗示“我要调工具”,而是直接输出一个 JSON 结构的 tool_calls,开发者解析起来极其稳定。这也是为什么说 Function Calling 是“从聊天到干活”的分水岭——它是第一个让大模型能和外部系统进行确定性交互的官方机制。

2. 动手之前:本地模型也能跑函数调用

2.1 选工具:为什么我用 Ollama 而不是直接调云端 API

在热词里反复出现“本地部署大模型”“ollama 部署大模型”,说明现在很多人都在尝试把大模型拉到本地。我平时做项目验证时,也很喜欢先用本地模型跑通流程,再切换到云端大模型。原因有三:一是数据不出内网,适合业务敏感场景;二是没有 API 调用费,适合反复调试;三是可以完全掌握模型行为,不会被厂商偷偷改版本搞懵。

本地部署工具里,我个人最推荐 Ollama。它支持 macOS、Windows、Linux,一条命令就能把模型拉下来,而且它自带的 OpenAI 兼容接口(/v1/chat/completions)几乎可以无缝衔接主流 SDK。哪怕你最后生产环境用的是云端 API,本地先用 Ollama 做开发联调,体验也完全不会差。

2.2 MacBook Air M3 16G 实测:哪些模型能跑函数调用

很多朋友担心自己的笔记本跑不动大模型,我拿 MacBook Air M3 16G 实测过几种主流模型,结果如下:

模型参数量量化版本函数调用表现生成速度(实测)能不能用
qwen2.5:7b70亿Q4_K_M能稳定输出 tool_calls,参数提取准确率高15~20 token/s推荐日常开发
qwen2.5:3b30亿Q4_K_M能输出 tool_calls,但复杂参数容易漏字段25~30 token/s轻量场景可用
llama3.1:8b80亿Q4_K_M需要提示词引导,原生稳定性一般12~18 token/s不推荐做生产
mistral:7b70亿Q4_K_M函数调用格式老旧,兼容性差15~20 token/s不建议选它

实测下来,qwen2.5 系列是本地跑 Function Calling 的“性价比之王”。它在训练时就专门强化了函数调用能力,输出结构非常规范,配合 Ollama 自带的 OpenAI 兼容接口,20 分钟就能搭出一个本地函数调用测试环境。

2.3 Ollama 部署 Qwen 2.5 7B 的具体步骤

这里给出我在 macOS 上实测可行的一套流程。

第一步,安装 Ollama。直接去官网下载 macOS 安装包,或者用 Homebrew 一行命令:

brew install ollama

第二步,启动 Ollama 服务并拉取模型。默认情况下,Ollama 安装后会自动监听 11434 端口。

ollama serve # 如果已经作为服务运行,这一步可跳过 ollama pull qwen2.5:7b

第三步,验证 OpenAI 兼容接口是否可用。打开新终端,用 curl 测一下:

curl http://localhost:11434/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "你好"}] }'

如果返回正常的 choices 内容,说明本地环境已经就绪。这里要注意一点:Ollama 的 OpenAI 兼容接口不需要 API Key,随便填一个(比如“ollama”)就能通过认证。

3. 核心实现:从 OpenAI 到本地模型的 Function Calling 调用全流程

3.1 基于 OpenAI 官方 SDK 的完整示例

我不喜欢只讲抽象概念,直接上一个完整的 Python 示例。这个示例实现了一个“查天气 + 算运费”的助手,你能看到 Function Calling 从“定义工具”到“执行工具”再到“二次生成回答”的完整闭环。

from openai import OpenAI client = OpenAI( base_url="http://localhost:11434/v1", # 换成你的 Ollama 地址 api_key="ollama" # 本地 Ollama 不校验 key,随意填 ) def get_weather(city: str) -> str: """模拟天气查询接口""" weather_map = { "北京": "晴,气温 25°C", "上海": "小雨,气温 22°C", "广州": "多云,气温 28°C", } return weather_map.get(city, f"{city} 天气数据暂时未收录") tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气情况", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海、广州" } }, "required": ["city"] } } } ] messages = [{"role": "user", "content": "北京今天天气怎么样?"}] response = client.chat.completions.create( model="qwen2.5:7b", messages=messages, tools=tools, tool_choice="auto", ) # 检查模型是否想调用函数 if response.choices[0].message.tool_calls: tool_call = response.choices[0].message.tool_calls[0] print("模型想调用:", tool_call.function.name) print("参数:", tool_call.function.arguments) # 执行真实函数 import json args = json.loads(tool_call.function.arguments) result = get_weather(**args) # 把函数调用记录和结果追加到消息列表 messages.append(response.choices[0].message) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result }) # 让模型基于工具结果生成最终回答 final_response = client.chat.completions.create( model="qwen2.5:7b", messages=messages, tools=tools, ) print("最终回答:", final_response.choices[0].message.content) else: # 模型没有调用工具,直接回答 print("模型直接回答:", response.choices[0].message.content)

这段代码的核心逻辑不复杂,但有几个地方我要特别强调。

3.2 messages 列表的“三段式”追加

很多人第一次写 Function Calling 会在消息历史的管理上出问题。完整的一次函数调用,messages 列表里至少要有四段内容:

  1. 用户原始请求,比如“北京今天天气怎么样?”
  2. 模型的 tool_calls 响应,也就是模型说要调用 get_weather,参数是 {“city”: “北京”}。这条消息的 role 是 assistant,但里面没有普通 content,而是带一个 tool_calls 字段。
  3. 工具执行结果,role 是 tool,必须带上 tool_call_id 来和上一步的调用请求对应。
  4. 模型基于工具结果生成的最终回复。

漏了第 2 条或者第 3 条,模型在下一轮就不知道刚才发生了什么,可能会重复调用函数或者答非所问。尤其是 tool_call_id,必须严格一一对应,不然会直接报错。

3.3 tool_choice 参数:auto、none、required 怎么选

在 OpenAI 兼容协议里,tool_choice 控制模型“什么时候可以调用函数”。

  • auto:模型自己判断是否需要调用工具,适合大部分场景。
  • none:模型只能用对话回复,即使你定义了 tools 也不调用,适合“闲聊模式”。
  • required:强制模型必须调用一个工具,适合“必须走工具”的业务规则。

实际项目里我建议默认用 auto,然后在业务层面对“模型没有调用工具”的情况做兜底。比如用户问“你好”,你就别指望模型调工具,直接走普通对话即可。而 required 模式在某些场景里很有用,比如“帮我订个酒店”,你希望模型无论如何都要先调查询接口,而不是直接猜一个答案。

4. 让模型会填参:JSON Schema 定义与参数工程

4.1 参数描述比参数类型更重要

很多开发者第一次写 tools 参数时,把注意力全放在参数类型上,比如 city 是 string、num 是 integer,然后发现模型经常填错参数。问题通常出在 description 写得太简单。

举个例子,如果 description 只写“城市”,模型有可能把“首都”这种词直接填进去。但如果你写“需要查询天气的城市名称,必须是中文全称,例如:北京、上海、广州,不接受拼音或简称”,模型的表现会立刻提升一个档次。这不是玄学,是训练数据里真实存在的关联模式:模型会优先依据 description 中的示例来生成参数。

4.2 用 enum 限制取值范围,减少无效调用

当参数只有固定几个候选值时,一定要用 enum。比如“查询天气”的城市,假设你只支持 100 个城市,那你最好在枚举里列出来,或者至少把常见城市列出来。模型在生成参数时会优先选择 enum 里的值。

{ "type": "object", "properties": { "city": { "type": "string", "enum": ["北京", "上海", "广州", "深圳"], "description": "城市中文全称" }, "date": { "type": "string", "description": "查询日期,格式为 YYYY-MM-DD" } }, "required": ["city", "date"] }

有人可能觉得 enum 太死板,后面加城市还要改代码。但函数调用本来就是强规范性协议,宁可多维护一个枚举列表,也不要让模型自由发挥然后返回一堆你处理不了的脏数据。enum 是成本最低的输入校验手段。

4.3 复杂嵌套参数:让模型学会处理对象和数组

除了基础字符串,Function Calling 也支持嵌套 JSON。比如“创建订单”这样一个动作,需要传递商品列表、收货地址、优惠券 ID,参数结构就复杂了。

{ "type": "object", "properties": { "order_no": { "type": "string", "description": "订单号,业务系统生成的唯一编号" }, "items": { "type": "array", "items": { "type": "object", "properties": { "sku_id": {"type": "string", "description": "商品 SKU ID"}, "qty": {"type": "integer", "description": "购买数量,必须是大于0的整数"} }, "required": ["sku_id", "qty"] } }, "address": { "type": "object", "properties": { "province": {"type": "string"}, "city": {"type": "string"}, "detail": {"type": "string"} }, "required": ["province", "city", "detail"] } }, "required": ["order_no", "items", "address"] }

我测过 qwen2.5:7b 对这种嵌套结构的支持,只要 description 写得清楚,它基本能抽出正确的参数树。不过有一点要注意:嵌套层级不要太深,超过三层以后,小参数模型容易抽漏字段。如果你发现模型经常漏掉内层字段,优先考虑拍平参数结构,而不是继续嵌套。

5. 多轮调用与会话记忆:真正“干活”的复杂场景

5.1 从一个函数到多个函数:让模型学会“选择”

真实业务不会只有一个函数。我做过一个简单的“物流客服助手”,里面同时有查订单、查物流、修改地址、申请售后的四个函数。模型需要根据用户一句话,决定调用哪个函数、传哪些参数。

多函数的定义方式和单函数差不多,就是把多个 JSON 结构放进 tools 数组。真正的问题在于:当函数变多之后,description 的“辨识度”就变得极其重要。比如“查天气”和“查温度”这两个工具,如果不仔细写描述,模型很可能总是调用错的那个。我的经验是,每个函数的 description 开头第一句,就要明确指出这个函数“适合什么请求、不适合什么请求”。

5.2 多轮调用中的状态管理:模型怎么记住“刚才的订单号”

在多轮对话里,用户可能先说“帮我查一下订单”,你问他订单号,他再告诉你“20250101”。如果每次都重新发一个空的 messages,模型肯定不知道订单号是什么。所以你必须把历史消息完整地带到下一次请求里。

messages = [] # 第一轮 messages.append({"role": "user", "content": "帮我查一下订单"}) # 模型会反问订单号,或者调一个获取订单列表的工具 # 第二轮(用户补充信息) messages.append({"role": "user", "content": "订单号是 20250101"})

这里的关键点在于:messages 才是模型“记忆”的唯一来源。只要你不把历史消息丢弃,模型就能引用之前的上下文。Function Calling 本身没有独立的“会话状态”,它的记忆完全靠 messages 传递,所以设计好 messages 的追加规则,就是设计好会话状态。

我做过的几个项目里,踩过最大的坑是“把 tool_call 结果拼错位置”。比如第一轮模型调用查订单函数返回了几个订单列表,第二轮用户说“选第一个”,结果我把第一轮的 tool 结果丢掉了,模型就不知道“第一个”指的是哪条。后来我的统一做法是:只要一个会话内的消息,全部保留在 messages 里,不管中间经过多少次函数调用,都不做“裁剪”,除非消息长度超过了模型上下文窗口。

5.3 循环调用:如果第一次工具结果不够,怎么继续问

有些复杂的任务,一次工具调用不够。比如用户问“广州和上海哪个冷”,理论上模型需要先调两次 get_weather,然后把两次结果对比回答。这时代码里就需要写一个循环,不断检查模型是否还想调用工具。

while True: response = client.chat.completions.create( model="qwen2.5:7b", messages=messages, tools=tools, ) message = response.choices[0].message if not message.tool_calls: # 模型不再调用工具,输出最终答案 print("最终答复:", message.content) break # 逐个处理工具调用 for tool_call in message.tool_calls: messages.append(message) # 注意,把 assistant 消息(带 tool_calls)追加进去 result = execute_tool(tool_call) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result })

在实际运行中,这个循环通常 2 到 3 轮就能结束。如果出现模型反复调用同一个函数、参数却不变的情况,大概率是 messages 里少了 assistant 那一条,模型“忘了刚才已经查过了”。我在代码里特意加了这行注释:message 本身有 tool_calls,不能只把 content 的内容丢进去,必须把整个 assistant message 追加进去。

6. 工具选型与避坑:从 API 到本地部署怎么选

6.1 OpenAI API、Ollama、vLLM 之间怎么选

很多初学者会问:我到底应该用云端 API 还是本地部署?热词里也频繁出现“免费大模型 API”“大模型下载”“本地部署大模型”。我把主流方案放在一起对比一下,方便你根据场景选择。

方案优点缺点适合场景
OpenAI API效果好,Function Calling 格式最稳定收费,数据出外网,可能有延迟生产环境要求高、数据不敏感
国内大模型 API(千问、GLM 等)免备案、中文效果不错各家兼容性略有差异国内业务,对合规要求高
Ollama + Qwen 2.5本地部署、免费、离线可用7B 模型能力有限,复杂参数容易出错开发调试、内网数据敏感场景
vLLM + 开源大模型吞吐高、可并发部署门槛高,需要 GPU生产环境的本地推理服务

从开发效率角度来说,我强烈建议你“先用 Ollama 联调,再切云端模型”。因为函数调用的链路和模型关系不大,你的代码逻辑只要遵循 OpenAI 兼容协议,本地能跑通,云端基本上也能跑通。差别只在于模型能力的强弱,而不是接口格式。

6.2 免费大模型 API 的“坑”有哪些

热词里有人问“有可以免费使用的大模型吗”,答案是肯定的。很多平台推出了免费额度或限时免费模型。但你在做 Function Calling 项目时,必须确认以下几个关键点:

  • 是否支持 tools 参数。有些入门模型只开放基础对话接口,看到 tools 直接忽略或者报错。
  • 工具调用的响应格式是什么。有些平台的 tool_calls 结构并不完全标准,需要你在代码里做兼容适配。
  • 每天/每月的调用次数限制。免费额度往往够你学习测试,但不够生产使用。

我实际测过几个免费 API,最大的问题是模型不按照参数的 JSON Schema 输出,导致解析层报错。这种场景下,你可以在代码里加一个“参数校验”步骤,发现不符合 Schema 就重试一次或者让模型重新提取。重试逻辑看起来简单,但在免费模型上极其实用。

6.3 别急着上微调,先用提示词和函数定义解决问题

排行榜和热词里经常出现“大模型微调”,很多朋友搞了几天 Function Calling 觉得不完美,就想着微调模型。我的建议是:不要一上来就微调。函数调用的稳定性和三个因素有关:模型本身能力、函数定义质量、代码处理逻辑。很多时候问题出在后两者,而不是模型。

举个例子,小模型总是漏传参数,你把 description 写得更详细、把必填字段提到 required 里,稳定性会明显提升。只有当你在现有模型上无论如何优化描述、调整提示词都无法达到业务要求时,才值得考虑微调。微调是“最后的手段”,不是“最佳实践”。

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

7.1 模型不调用函数,只直接回答,怎么办

这种问题经常发生。我建议按以下顺序排查:

  1. 确认 messages 和 tools 传参正确。用最简单的“查天气”示例跑一遍,排除代码问题。
  2. 检查函数 description 里的文字。模型把函数调用当成了一种“语言行为”,如果描述里没有明确的触发词,它可能不会调用。把“查询指定城市的当前天气情况”改成“当用户想了解某个城市的天气时,必须使用此工具”,效果立竿见影。
  3. 换成更强的大模型试试。如果 qwen2.5:3b 不调用,换 7b 往往就好了。模型虽然架构相似,但在指令遵循能力上有明显差距。

7.2 模型调用了函数,但参数解析失败

参数解析失败最常见的两个原因:一是模型返回的 arguments 不是合法 JSON,二是参数结构和 Schema 不一致。针对前者,我建议在代码里做一层“清洗”,比如使用json.loads失败后,用正则提取其中的 JSON 片段再解析。针对后者,最简单的方法是“用 enum 限定取值 + 用 required 强制必填”。

我写了一个简易的参数解析函数,一般在生产环境里足够用:

def safe_parse_arguments(arguments: str) -> dict: """带兜底的参数解析:移除代码块标记,提取最外层 JSON""" arguments = arguments.strip() if arguments.startswith("```"): arguments = arguments.strip("`") if arguments.startswith("json"): arguments = arguments[4:] try: return json.loads(arguments) except json.JSONDecodeError: # 尝试提取 { 到 } 之间的内容 start = arguments.find("{") end = arguments.rfind("}") if start != -1 and end != -1: return json.loads(arguments[start:end+1]) raise

7.3 模型死循环调用同一个函数,怎么止损

上一节提过,死循环一般和 messages 历史不完整有关,尤其是漏掉了带 tool_calls 的 assistant 消息。除此之外,还有一种场景是模型认为函数返回结果“不满足要求”,于是反复调用。这时你在代码里加一个“最大调用次数”限制,比如 5 轮之后强制终止,并把当前的上下文或多轮结果交给模型,让它尝试直接回答。这个兜底逻辑不复杂,但能避免生产环境里出现巨额 token 消耗。

问题现象可能原因排查方式
模型不调用函数description 触发词不明确把“必须使用此工具”写进描述
模型调用函数但不填参数参数 description 太模糊增加示例值和初始值
参数解析失败模型输出非标准 JSON加 safe_parse_arguments 兜底
死循环调用同一个函数messages 历史缺失 assistant 消息将完整 assistant message 追加进历史
多函数选择错误description 不具备辨识度每个函数开头写明“适合/不适合”
小模型漏传嵌套字段模型能力不足拍平 JSON 结构,降低嵌套层级

7.4 对于免费 API 和在线平台,Function Calling 响应慢怎么办

如果你用的是免费 API,响应速度往往不稳定,比如 30 秒才返回结果。这时你需要注意:超时时间要设得足够长,避免前端主动断连。如果你在服务端调用,建议加一个异步队列,不要让用户请求一直挂着。最简单的方式是把调用放到后台任务里,轮询拿结果,前端只负责展示“处理中”状态。这个方案在老旧的业务系统改造里特别常用。

8. 最后分享两个实用小技巧

第一个小技巧:用 Function Calling 做“参数提取”的时候,不一定要真的去执行函数。我经常把 Function Calling 当成一个“结构化信息抽取器”来用——定义好我想提取的字段,让模型从用户消息里抽出来,然后只取 tool_calls 里的参数,不真正执行任何工具。这种用法在表单自动填充、信息录入场景里非常有效,本质上是用函数调用的协议来约束模型的输出格式,比让模型输出普通 JSON 要稳定得多。

第二个小技巧:如果你用的是 Ollama,可以直接在Modelfile里通过PARAMETER stop或者自定义系统提示词来增强小模型的函数调用稳定性。比如给模型加一句“当用户询问天气时,务必调用 get_weather 工具”,效果往往比你在 SDK 端用 system prompt 更稳定。这个方法看起来不起眼,但在 3B 级别的模型上提升非常明显。

坦白说,Function Calling 这条路,我一开始也走得很曲折。印象最深的一次是把线上客服机器人全部切到函数调用架构,结果刚上线那天晚上,因为 messages 历史漏了一条 assistant 消息,整个机器人在“查订单”场景里疯狂死循环,token 费用跑出一个让我肉疼的账单。后来加上了循环次数限制和完整的消息追加逻辑,这套系统才真正稳定下来。那次的教训让我明白:Function Calling 的门槛不在“会不会调接口”,而在“能不能把工具链路、消息状态和异常兜底设计完整”。希望这篇实战指南能帮你少踩几个坑,把大模型真正变成你业务里能干活的角色。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 8:26:27

无主之地2 联机超时?一次连接故障应该怎样拆开看

一、一个值得注意的事件近期,Thomson Reuters披露旗下C-Track案件管理平台发生网络安全事件,并表示调查发现未经授权的一方曾访问部分文件。事件本身属于企业网络安全范畴,却能让普通用户重新思考一个问题:远程访问带来便利的同时…

作者头像 李华
网站建设 2026/9/5 8:18:34

模糊查询中的意图识别与场景路由:如何避免答非所问

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

作者头像 李华
网站建设 2026/9/5 8:18:32

薄膜手套怎么选?灭菌与骨科要求

薄膜手套看似简单,但医院采购和临床使用时最常问的就是:它到底能不能当外科手套用?答案很明确:不能。医用薄膜手套属于一类医疗器械,主要解决的是低风险、非侵入性操作中的基础阻隔问题,而不是手术级别的无…

作者头像 李华
网站建设 2026/9/5 8:17:08

课程论文写不下去?书霸AI给你破局方法,官网www.shubaai.com

课程论文写到一半卡住了,这是再常见不过的事。前面还顺顺利利,写到中间突然不知道该往哪走,盯着文档半天,一个字也写不出来。今天这篇文章,就讲讲课程论文写不下去时怎么办,也聊聊书霸AI官网www.shubaai.co…

作者头像 李华
网站建设 2026/9/5 8:16:10

华为eNSP安装全攻略:解决VirtualBox与WinPcap兼容性问题

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

作者头像 李华