news 2026/9/18 11:31:25

基于DeepSeek API构建对话式代码补全智能体实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于DeepSeek API构建对话式代码补全智能体实战指南

简介:面向希望掌握智能体开发与DeepSeek应用的技术人员,这份28页PDF以《智能体开发实战:基于DeepSeek构建对话式代码补全工具》为题,系统讲解从原理到落地的完整路径。内容覆盖智能体与代码补全工具概述、DeepSeek技术原理剖析、开发环境搭建、对话式代码补全工具架构设计、核心补全功能实现、用户交互模块开发、测试优化以及部署上线等关键环节,并展开介绍了深度学习基础架构、注意力机制、模型训练与优化、输入处理与特征提取、模型调用与推理、结果筛选、交互界面设计、语音输入等具体知识点,适合具备一定编程基础并希望借助大模型提升开发效率的读者。资源包共1个文件,类型为PDF,大小2.01MB,文档目录完整、图表正常,可直接按章阅读或作为实战参考。目前已有78人学习下载,是入门智能体应用开发并快速搭建代码补全方案的有益资料。

1. 对话式代码补全:为什么把“补全”做成了“智能体”

代码补全这个领域,过去十年走完了从缩进对齐到单行续写再到整个函数生成的三级跳。但“对话式代码补全工具”和传统IDE补全有一个本质区别:它不再假设用户知道要写什么函数名,而是允许你用自然语言描述意图——比如“这个接口加个超时重试```”,然后模型给出完整实现。基于DeepSeek做这件事,核心不在于调用一个聊天API,而在于搭建一个智能体运行时,让模型具备多轮记忆、工具调用和流式反馈三种能力,才能真正回答“上下文改了哪里”“asyncio包怎么写”这类复杂指令。本文面向想绕开闭源补全插件的开发者,把从DeepSeek API接入到智能体框架完整落地的主路径走一遍。

2. 搭建智能体基座:DeepSeek的API接入与运行时设计

2.1 选型边界:为什么是DeepSeek而不是本地小模型或低代码平台

代码补全工具的后端,不能只看推理榜单排名,还要看API形态、延迟曲线和成本模型。DeepSeek对外提供的是OpenAI兼容接口,这意味着团队不需要引入新的SDK族,只要把base_url指向DeepSeek的地址,已有的OpenAI调用代码就能直接迁移。对一支熟悉GPT接口的团队来说,这是把“试水DeepSeek”变成“接入DeepSeek”的最短路径。

另一方面,本地部署小模型(例如7B~14B量级的开源代码模型)当然有数据私有的优势,但代价是GPU资源占用、部署运维成本,以及补全质量上的明显落差。对话式补全工具对“意图理解”要求很高,用户说“帮我把这个函数改成协程版”,7B参数量级的小模型经常把async def加上了却漏掉await,这种错误在真实工程里非常致命。DeepSeek作为云端API,处于一个“推理质量接近第一梯队、成本远低于闭源旗舰”的性价比位置,最适合做对话式补全的后端。

如果你用过Dify、Coze这类低代码智能体平台,会发现搭一个“读文件、写代码”的Agent并不难。但这类平台在代码补全场景有个硬伤:你无法精细控制上下文的组装顺序、工具调用的触发时机、以及agent_loop的循环策略。自己调用SDK搭运行时,换来的控制力在代码补全里是刚需,这不是低代码平台能替代的。

2.2 最小可用调用链:三步把DeepSeek API跑通

先从零开始把链路打通。下面代码只依赖openai官方SDK,是接入DeepSeek的最短路径:

from openai import OpenAI client = OpenAI( api_key="sk-your-deepseek-api-key", base_url="https://api.deepseek.com" ) resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": "你是一名资深后端工程师,熟悉Python和异步编程。"}, {"role": "user", "content": "用Python写一个带超时重试的HTTP请求函数,使用httpx库。"} ], temperature=0.2, stream=False ) print(resp.choices[0].message.content)

代码里的关键参数拆开说明。

api_key从DeepSeek开放平台获取,只能放在服务端环境变量或密钥管理服务里,不能出现在前端代码或Git仓库中。base_url指向https://api.deepseek.com,模型侧有两个常用选择:deepseek-chat是通用对话模型,响应速度快,适合日常补全;deepseek-reasoner走思维链推理,生成复杂算法或重构逻辑时更稳定,但延迟明显更高,需要根据场景取舍。temperature=0.2是代码任务的推荐值,代码生成最怕“创造性偏差”,温度越低输出越收敛,超过0.5就很容易出现模型编造API接口的现象。

确认全量响应正常后,把stream改为True,测试流式输出:

stream = client.chat.completions.create( model="deepseek-chat", messages=messages, stream=True ) for chunk in stream: if chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="")

流式是对话式工具的第一道关口。人机交互的研究结论是:用户等待代码生成时,3秒内可接受,超过5秒的空白期就会产生“卡死了”的错觉。流式输出把首Token延迟从“全量生成时间”压到一个极低值,用户第一秒就能看到代码开头,心理体验完全不同。

2.3 智能体运行时骨架:会话状态与事件循环

对话式补全和一次性生成的本质差异在“状态”。用户会先问“这个函数性能怎么样”,得到回答后再提“那帮我改成异步”——这里的“那”依赖上一轮的讨论对象。一个最简的会话状态管理类:

class Session: def __init__(self, session_id: str, max_history: int = 20): self.session_id = session_id self.messages = [{"role": "system", "content": SYSTEM_PROMPT}] self.max_history = max_history def add_message(self, role: str, content: str): self.messages.append({"role": role, "content": content}) # 裁剪最旧的非system消息,保留system置顶 if len(self.messages) > self.max_history: system = self.messages[0] self.messages = [system] + self.messages[-(self.max_history - 1):] def to_api_format(self): return self.messages

注意max_history并不是越大越好。代码补全场景里,最近的对话最相关,过远的上下文不仅消耗token,还可能把模型带偏——例如用户在第10轮已经放弃的旧方案,如果还在上下文里,模型会时不时“旧事重提”。经验值在20到30条之间,再配合后面的token级截断策略,是性价比最高的配置。

有了会话之后,把“提问→回复”的线性调用改造成事件驱动循环,这是智能体与普通API封装的分水岭:

def agent_loop(session: Session, user_input: str, tools: dict): session.add_message("user", user_input) while True: resp = client.chat.completions.create( model="deepseek-chat", messages=session.to_api_format(), tools=tools if tools else None, stream=False ) msg = resp.choices[0].message if msg.tool_calls: # 工具调用分支:执行本地函数,把结果回填上下文 for tc in msg.tool_calls: func = tool_registry[tc.function.name] result = func(**json.loads(tc.function.arguments)) session.add_message("role", "tool") session.add_message("content", result) else: session.add_message("assistant", msg.content) return msg.content

tool_calls分支是整个循环的引擎。模型通过函数名和参数声明“我想调用哪个本地方法”,智能体运行时执行后把结果作为tool角色消息塞回对话上下文,模型再基于工具结果继续推理。这一步完成了从“问答API”到“智能体运行时”的跃迁。后面章节的代码补全功能,全部建立在这套循环之上。

3. 对话式函数生成的核心实现:多轮上下文与代码生成

3.1 system消息决定“职业身份”,别让模型跑偏

很多团队在接入大模型后做的第一件事就是把提示词写长,却没有仔细设计system消息。对话式补全工具如果不在system层做身份限定,模型很容易退化成“陪聊模式”——用户抱怨一句“这个bug烦死了”,模型就开始共情,不给代码了。

一份可用于生产环境的补全System Prompt至少要覆盖四项内容:

  • 身份限定:你是内嵌在IDE中的代码补全智能体,只回答与编程相关的问题。
  • 输出约束:直接输出可执行代码;不要输出Markdown代码块标记;复杂逻辑允许写一小段解释性注释。
  • 工程知识:优先使用当前项目的语言、框架和依赖版本,不擅自重构。
  • 边界声明:无法从上下文判断组件行为时,明确回答“不确定”,禁止编造API用法。

实际可用的模板如下:

你是IDE中的代码补全助手。你的任务是根据对话上下文和当前文件内容,生成可工作的代码。 约束: 1. 只输出代码本身,不要输出Markdown代码块标记。 2. 若需要修改多处文件,第一行用注释标注文件名,例如 # file: router.py 3. 保持项目现有编码风格,不擅自改变架构设计。 4. 对把握不准的API用法,用注释标注“建议验证”,并给出替代方案。

这段提示词的核心是把模型的行为从“通用助手”收敛到“能直接提交代码的工程师同事”。实际测量中,加上第4条后,模型编造API的现象减少了大半,因为“建议验证”给了模型一个低成本的保底输出路径,它不再需要用信口开河来填补不确定性。

3.2 上下文组装顺序:把光标附近的代码放在最该放的位置

代码补全要有效,关键不只是“用户说了什么”,更是“模型能看到什么”。大语言模型的注意力机制对位置敏感,越靠后的内容在生成时的权重越高。因此上下文的组装顺序,本身就是一种隐式的提示工程。

经过反复验证的组装顺序如下:

  1. system prompt(身份与约束)
  2. 项目级信息:语言、框架、构建工具
  3. 当前文件的完整内容
  4. 光标位置附近的代码(比完整文件更关键)
  5. 对话历史(按时间正序)
  6. 最近一条用户指令

把“光标附近的代码”放在用户指令紧前位置,模型生成时对“刚刚看到了什么”的保持度最高。基于这个原则的上下文组装函数:

def build_codegen_context( session: Session, file_path: str, file_content: str, cursor_pos: int ) -> list: # 提取光标前50行作为“附近代码”,压缩长文件的影响 nearby = file_content[:cursor_pos].split("\n")[-50:] ctx = [ {"role": "system", "content": CODE_GEN_SYSTEM}, {"role": "user", "content": f"当前文件: {file_path}"}, {"role": "user", "content": f"文件完整内容(光标在第{cursor_pos}个字符处):\n{file_content}"}, {"role": "user", "content": "光标附近代码:\n" + "\n".join(nearby)} ] return ctx + session.messages[1:] # 拼接对话历史,跳过原system

两个细节值得展开。

第一,这里把“文件完整内容”和“光标附近代码”作为两条独立消息注入,而不是拼成一段长文本。实测发现,独立消息能够强化模型对不同信息块的区分度——它知道“完整文件”是背景资料,“附近代码”是当前焦点。第二,“光标前50行”是经验值。50行足够模型理解当前函数的签名、依赖了哪些同模块变量,又不至于把焦点冲淡。

如果文件内容过长,截断策略不是“掐头去尾”,而是保留光标之前约1万字符加光标之后2千字符。这样既保证模型看到解题需要的大部分代码,又不至于让上下文窗口被一个文件占满,反而丢失了对话历史里的关键指令。

3.3 流式输出与编辑体验:流式条件下最容易踩的坑是tool_calls碎片

对话式补全的体验上限,基本由“首Token延迟”和“打字机效果的平滑度”决定。流式输出在这里不是可选项,而是必选项。但把非流式代码直接改成stream=True,会踩中一个隐蔽的坑:tool_calls字段同样是分片返回的,直接把每个chunk里的tool_calls当完整JSON解析,程序必然崩溃。

正确的分支处理方式如下:

def stream_with_tools(session, user_input): session.add_message("user", user_input) stream = client.chat.completions.create( model="deepseek-chat", messages=session.to_api_format(), tools=tools, stream=True ) tool_call_chunks = [] for chunk in stream: delta = chunk.choices[0].delta if delta.tool_calls: # 关键:先收集碎片,不能边收边解析 tool_call_chunks.append(delta.tool_calls[0]) elif delta.content: yield delta.content if tool_call_chunks: # 流结束后再拼接完整参数并解析 full_args = "".join(c.function.arguments for c in tool_call_chunks) yield json.loads(full_args)

这段代码的逻辑要点:流式模式下,工具调用的函数名和参数被拆成多个增量片,必须先收集、拼接、再解析,不能边收边用。另一个常常被忽略的点是生成器函数里yield两种不同类型的值(字符串内容和工具调用结果),需要在调用端用isinstance做类型区分。

这也是“编辑器接入DeepSeek”这类需求里常见的卡点。很多开发者参考示例代码时只注意到stream=True,没意识到tool_calls也会被流式拆分。把这个分支处理对,智能体才算真正具备“边思考边行动”的能力。

4. 上下文裁剪、约束生成与function calling:把补全结果推向工程级

4.1 按token预算做层级截断,而不是暴力丢弃

对话式补全走到第15轮时,上下文里可能堆着大量已经过时或被替换的旧代码片段。模型会把过期内容继续当“事实”引用,这正是“代码幻觉”的重要来源之一。DeepSeek会返回usage.prompt_tokens,这是校准上下文预算的第一手数据。

常见的处理策略是层级截断,按优先级从低到高依次回收:

  1. 对话历史中最旧的非system消息,保留最近10到15条。
  2. 用户粘贴过的大段报错日志——超过2000字符时,只保留首尾各300字符。
  3. 文件内容中远离光标的片段。
  4. 项目级元信息(框架版本、代码风格描述),在极端紧张时才裁。

这里给出一个利用usage反馈做动态裁剪的实现:

FRAGMENT_BUDGET = 30000 # 经验值,约为模型上下文窗口的60% def trim_context(ctx: list, last_usage: int) -> list: if last_usage <= FRAGMENT_BUDGET: return ctx over = last_usage - FRAGMENT_BUDGET freed = 0 new_ctx = [] for msg in reversed(ctx): if msg["role"] == "system": new_ctx.insert(0, msg) continue if freed < over: content = msg.get("content", "") if len(content) > 1000: # 压缩长消息:保留首尾 msg = {**msg, "content": content[:500] + "\n...[truncated]...\n" + content[-300:]} freed += len(content) - 800 else: freed += len(content) continue # 直接丢弃这条短消息 new_ctx.insert(0, msg) return new_ctx

这段代码的核心主张是:宁可把单条长消息压缩到“首尾保留”,也不要直接删掉整条历史。代码补全场景中,用户之前讨论的变量名、函数签名、约束条件往往分布在前30%和后30%的开头结尾处,中段的大块代码即使丢失,模型也能靠首尾信息重构语义。这也顺带回应了DeepSeek对话中“达到对话长度上限”的提示——提前在客户端做token级别的控制,而不是等API报错后再清空重开。

截断的时机比方法更重要。不要在每轮对话后都执行裁剪,而是在usage.prompt_tokens连续两次超过预算时触发,给上下文一个“缓冲期”,避免反复截断导致模型丢失刚讨论过的方案。

4.2 用function calling把工程索引接进对话

对话式补全有一个很常见的需求:用户问“帮我写一个调用fetch_user_profile的接口”,但模型并不知道这个函数的签名和返回结构。如果每次都要求用户手动粘贴相关代码,工具的价值就少了一半。解法是把工程索引能力封装成工具,注册到前文实现的agent_loop中。

一个简单可靠的工具设计是调用ripgrep做项目内符号搜索:

AVAILABLE_TOOLS = [{ "type": "function", "function": { "name": "search_definition", "description": "在项目源码中搜索标识符的定义位置,返回文件路径与附近代码。当用户提到当前项目中的函数、类、变量时,应调用此工具。", "parameters": { "type": "object", "properties": { "identifier": {"type": "string", "description": "要搜索的标识符名称"}, "workspace": {"type": "string", "description": "项目根目录路径"} }, "required": ["identifier", "workspace"] } } }] def search_definition(identifier: str, workspace: str) -> str: import subprocess result = subprocess.run( ["rg", "-n", "--with-filename", identifier, workspace, "-g", "*.py"], capture_output=True, text=True, timeout=10 ) return result.stdout[:4000] or f"{identifier} 未在项目中发现定义。"

AVAILABLE_TOOLS传入agent_loop之后,模型会在判断“需要项目信息”时自动发起工具调用。这套机制的价值在于“按需读取”——模型平时只看到搜索结果摘要,不需要把整个项目代码都塞进上下文,这对token成本的节省是数量级的。

这里面有一个参数细节几乎没人写:tool_choice。默认值是"auto",模型自行决定是否调用工具。在“用户明确要求查找某个文件”的场景,可以强制指定工具名:

resp = client.chat.completions.create( model="deepseek-chat", messages=session.to_api_format(), tools=AVAILABLE_TOOLS, tool_choice={"type": "function", "function": {"name": "search_definition"}} )

我的建议是:代码生成主路径保持"auto",因为强制工具调用会在不需要读取工程信息时白白增加一次往返延迟;而在“问题定位”模式下,例如用户说“去项目里查一下这个函数的定义”,就显式指定工具名。

4.3 结构化输出与语法校验回填

代码生成链路走到这里,模型已经能产出像样的代码,但“像样”不等于“能运行”。在把模型输出交给用户之前,至少要做两层校验。

第一层是语法校验。Python代码用ast.parse,JavaScript代码用Node的--check参数。发现语法错误时,不是直接向用户报错,而是把错误信息回填到会话上下文里,让模型自己修:

import ast def verify_python(code: str): try: ast.parse(code) return True, "" except SyntaxError as e: return False, f"SyntaxError at line {e.lineno}: {e.msg}" ok, err = verify_python(generated_code) if not ok: session.add_messages([ {"role": "user", "content": f"你刚才生成的代码有语法错误:{err}。请修正后重新输出完整代码,不要复述其他内容。"} ]) # 回到agent_loop循环,继续下一轮推理

这个“校验—回填—重试”回路,通常一两轮就能收敛。有意思的是,模型在生成时偶尔会在函数中间插入多余空行或缩进错误,语法校验回路对这类小毛病有奇效。

第二层是response_format约束。当你需要模型返回“复杂度分析+修改建议+代码”这类混合内容时,让模型以JSON结构输出,方便前端分栏渲染:

resp = client.chat.completions.create( model="deepseek-chat", messages=messages, response_format={"type": "json_object"} )

注意,DeepSeek的JSON Output模式要求messages里必须包含“json”这个关键词的提示,否则可能触发格式错误。另外,JSON模式会额外消耗一些token,只在需要结构化返回时才开启,纯代码生成场景保持默认即可。

做到这一步,你实际上已经实现了一个最简的多智能体雏形:代码生成器负责写,校验器负责挑错,两者通过会话上下文接力。把它扩展到“代码生成—静态检查—单测运行”三个智能体轮流协作,就是多智能体系统的自然演化路径。

4.4 VSCode插件接入:暴露给用户的最小交互面

后端能力做好后,需要一个IDE前端来承接。最常见的落地方式是VSCode插件,核心交互只有两个入口:选中代码后按快捷键触发“对话式修改”,或者在侧边栏面板里直接聊天。

插件端的实现要点是不要重复维护会话状态,而是把Session类放在后端服务里,插件只传session_id和用户输入:

// 简化版的VSCode插件调用逻辑 import * as vscode from 'vscode'; export async function requestCompletion(sessionId: string, prompt: string) { const response = await fetch('http://localhost:8000/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, message: prompt }) }); const reader = response.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const text = decoder.decode(value); // 逐段更新编辑器中的虚拟文本或输出面板 vscode.window.activeTextEditor?.edit(editBuilder => { // 按流式增量插入到光标位置 }); } }

流式增量“写”进编辑器的方案,比“生成完一次性替换”体验好得多。但要注意:不要直接修改用户文档,先在虚拟文档或diff视图中展示结果,用户确认后再应用。这层设计是工具是否“可信”的关键。

5. 质量评估与延迟分层:把补全工具推到可交付状态

5.1 用“提交对比法”度量补全质量

代码补全没有传统意义下的“准确率”,但它有一组可操作的近似指标。最实用的是“提交对比法”:从团队最近两周合并的真实PR里,把每个PR修改前的代码作为输入,让工具生成修改方案,再用编辑距离衡量生成结果与真实diff的相似度。

import difflib def patch_similarity(generated: str, actual: str) -> float: sm = difflib.SequenceMatcher(None, generated, actual) return sm.ratio()

这个指标不追求完全一致,而是观察趋势。每周跑一次,ratio稳定在0.6以上,说明模型理解了团队代码风格;低于0.4,则需要检查上下文组装是否遗漏了关键文件,或者system prompt里的风格约束是否生效。配合ast.parse通过率、首Token延迟P90、以及“是否一轮对话就产出可运行代码”三个辅助指标,可以在CI里形成对模型升级的回归检测。每次DeepSeek发布新版本或修改参数后,在测试集上跑一遍,对比生成质量和延迟。代码补全这类任务对指令跟随的变化非常敏感,新版本不一定更好,数据说了算。

5.2 延迟分层:把上下文强度绑定到对话阶段

补全工具的延迟不是越低越好,而是“该低的时候低,该全的时候全”。我的做法是把上下文组装策略拆成三档,根据对话轮数动态切换:

上下文策略平均tokenP90首Token延迟适用场景
仅对话历史+光标行约30000.8s持续对话中的快速补全
加光标前后50行约80001.6s新开对话的常规补全
全文件+工程定义检索约200003.4s首次深度修改,质量优先

落地策略很简单:第一轮对话给满上下文,让模型充分理解项目;第五轮之后,模型已经看过文件全貌,上下文降档到“最近3轮对话+光标附近代码”,把P90首Token延迟压在1.2秒以下。实现上是动态算出来一个上下文强度系数:

def context_level(turn_count: int) -> int: if turn_count == 1: return 3 elif turn_count < 5: return 2 return 1

这里有一个容易被忽视的细节:延迟分层要和前端交互联动。第一轮补全耗时3秒可以接受,因为用户预期“这个工具在理解项目”;但同一个会话的第8轮仍然3秒,用户就会烦躁。因此第5轮之后,不仅在服务端降档上下文,前端还应调整等待提示策略——程度到不再提示“正在理解代码”,而是直接显示“生成中”。

5.3 评估的终点:补全结果必须经过用户的“最后一道校验”

最后谈一个产品层面的技巧:每次补全完成后,在结果下方保留一个“接受/拒绝”按钮,并记录用户行为作为反馈数据。接受则把生成代码与上下文的组合存入正样本集,拒绝则把事件存为负样本。这批数据积攒到几百条之后,就是评估模型升级最优的私有测试集——比任何公开benchmark都贴近真实开发场景。

模型升级前,先在这批样本上跑一次生成,对比新旧版本的接收率。接收率掉5个百分点,几乎可以断定新版本不适合当前团队的工作流。这种基于自身工程实践的验证闭环,才是DeepSeek对话式补全工具真正迈向可交付状态的关键一步。

本文还有配套的精品资源,点击获取

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

Python机器学习实战:Scikit-Learn学习路径与数据挖掘应用

数据挖掘与机器学习&#xff1a;Python机器学习软件包Scikit-Learn的学习与运用1. 内容整体设计与思路拆解1.1 为什么选择Scikit-Learn作为机器学习入门工具学习数据挖掘和机器学习&#xff0c;选对工具能少走很多弯路。我在实验室带新人这几年&#xff0c;发现一个规律&#x…

作者头像 李华
网站建设 2026/9/18 11:28:45

嵌入式BMS开发实战:CAN物理层、SOC算法部署与汽车级可靠性设计

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

作者头像 李华
网站建设 2026/9/18 11:27:29

IntelliJ插件实现IDE内嵌音视频播放:5线程轻量流媒体方案

1. 这不是“IDE功能扩展”&#xff0c;而是一次对开发工具边界的重新试探你有没有试过&#xff0c;在写 Java 代码的间隙&#xff0c;突然想听一首《夜来香》&#xff1f;或者在调试 Spring Boot 接口时&#xff0c;顺手点开央视新闻频道看实时直播&#xff1f;又或者&#xff…

作者头像 李华
网站建设 2026/9/18 11:26:36

OpenClaw 的环信 IM 助手回消息,onboard 模型通道改走 TaoToken

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

作者头像 李华
网站建设 2026/9/18 11:26:07

Gemini 3 登顶 MArena:拿 TaoToken 复现榜单同款对话

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

作者头像 李华