news 2026/9/7 7:45:20

Agent Harness 为何选 JSON-RPC 2.0:模型与工具通信的最佳协议实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent Harness 为何选 JSON-RPC 2.0:模型与工具通信的最佳协议实践

最近在给一个 Agent Harness 项目设计工具调用通信层时,我干了一件看起来特别“复古”的事:把 JSON-RPC 2.0 从旧文档里翻出来重新读了一遍。这份规范不长,总共才几页,但它几乎一字不改地成了我这一整套 Agent 框架的数据交换底座。说实话,做 AI Agent 开发做到一定深度,你一定会撞上一个绕不开的话题:模型负责“想”,工具负责“做”,那 Agent 和工具之间到底怎么说话?很多人第一反应是上 REST,第二反应是上 gRPC,但真正把 Agent Harness 落地一遍之后,你会发现 JSON-RPC 2.0 这个被冷落多年的老协议,反而是最合适的选择。

这篇文章就当是这个系列的“前置篇”。我准备把 Agent Harness 是什么讲清楚,再把 JSON-RPC 2.0 的规则过一遍,然后重点聊一个实际问题:为什么偏偏是它被 AI Agent 重新捧红,以及你在自己的 Agent 工程里应该怎么落地这套协议。适合正在做 Agent 应用开发、在研究 Agent 框架源码,或者单纯想搞清楚 MCP 这类协议底层逻辑的读者。我不打算写成教科书式的协议解读,而是按我实际踩过的坑、做过的权衡来聊,这样你拿去就能用。

1. 先说结论:Agent Harness 到底解决什么问题

1.1 为什么“Agent 唯一要做的就是决定”这句漂亮话不能直接用

现在聊 AI Agent,很多人喜欢说一句话:模型负责决策,外部工具负责执行,Agent 只需要在大脑里“想”清楚调用哪个工具。这话理论上没错,但你真正写代码的时候会发现,从“模型说想调用 get_weather”到“get_weather 真的被安全地执行完并拿到结果”,中间隔着一条巨大的工程鸿沟。

模型只是一个文本输入输出的推理单元,它自己不发起 HTTP 请求,不读文件,不操作数据库。它给出的 tool_call 充其量是一个结构化的“意图”,比如:

{ "name": "get_weather", "arguments": { "city": "杭州" } }

接下来谁来把这个意图变成真实动作?谁来保证它不会调用一个不该调用的工具?谁来给这个工具注入密钥和配置?谁在工具抛异常的时候决定是重试还是换一条路径?谁把执行结果再包装成模型能读的上下文?这些活如果全部散落在业务代码里,你的 Agent 很快就会长成一坨没法维护的“面条代码”。

这就是 Agent Harness 存在的意义。你可以把它理解成“Agent 的执行外壳”或者“外骨骼”。Agent“决定”做什么,Harness 负责让这个决定能发生、能完成、能被观测、能被控制。没有 Harness,模型是裸奔的;有了 Harness,模型才能在受控环境里安全地调用工具、处理上下文、完成多步任务。

1.2 Harness 与 Agent:谁是谁的“外骨骼”

这个词的英文原文就很有意思。harness 本身是“马具、挽具、安全绳”的意思,动词是“给马套上挽具”或者“把人用绳索固定住”。延伸一下,Agent Harness 的意思就是“给 Agent 套上一层控制与执行的挽具”。很多读者在搜 harness 和 agent 的区别,说白了就是这两个概念经常被混在一起讲。

我习惯用一个比喻:Agent 是“司机”,Harness 是“汽车”。司机决定要去哪儿、走哪条路,这是智能决策;但发动机、刹车、方向盘、仪表盘、安全带,这些基础设施归汽车管。没有汽车,司机再聪明也挪不了半步;没有司机,汽车也只是停在原地的一堆金属。但在软件工程里,这两层往往被写进同一个进程,所以很多人感受不到边界。

实际拆开看,Harness 至少要承担以下几件事:

  • 工具注册与发现:Agent 能调用哪些工具,每个工具的入口在哪里,参数结构是什么。
  • 参数校验与权限控制:模型生成的参数不一定合法,也不一定安全,Harness 要在执行前拦截。
  • 上下文管理:多轮对话里哪些历史信息要保留,工具执行结果如何压缩回填。
  • 执行调度与错误恢复:步骤失败后是否重试、是否换工具、是否终止。
  • 可观测性:每一步的输入输出都要有日志,出了问题能回放。

而这些工作里,最核心、最底层的一项,就是“通信”。无论注册、调用、返回结果,还是上报状态,Harness 都需要一套协议来管理 Agent 与工具之间的数据流。这套协议,在我的实践里,选来选去最后还是落在了 JSON-RPC 2.0 上。

2. JSON-RPC 2.0 不是新东西,但规矩值得重新过一遍

2.1 一个请求对象的结构,就这么点东西

JSON-RPC 2.0 规范短到让人怀疑人生。请求对象总共就四个字段:

{ "jsonrpc": "2.0", "method": "tools/call", "params": { "tool": "get_weather", "arguments": { "city": "杭州" } }, "id": 1 }

每个字段的含义都很朴素:jsonrpc固定是"2.0"method是字符串,表示要调用的方法名,params是参数,id是请求的唯一标识。规范允许params有两种形态:按位置传的数组,或者按名字传的对象。在我的 Agent 工具调用场景里,我强烈建议用对象形态,因为工具函数的参数几乎都有名字,用数组很容易在参数顺序上翻车。

响应对象也异常简单。要么是成功:

{ "jsonrpc": "2.0", "result": { "temperature": 26, "condition": "晴" }, "id": 1 }

要么是失败:

{ "jsonrpc": "2.0", "error": { "code": -32601, "message": "Method not found" }, "id": 1 }

这里有个规矩必须记住:resulterror只能出现一个,不能同时存在。error对象里code是整数,message是字符串,data是可选的附加信息。你哪怕把协议全忘了,只记住“请求有 id,响应要带同一个 id”这一条,也能把通信跑通大半。

最容易被忽略的是id的设计。如果请求里没有id,这就不再是一个普通请求,而是一个“通知”。服务端收到通知后,不会返回任何响应。很多初学的朋友在调试时报错“我发了请求但收不到响应”,一查才发现,请求对象里忘了写id,被服务端静默当成通知处理了。还有一点要注意:虽然规范里id允许是null,但实务中千万别用null,因为有的服务端会把id: null也当成通知处理,或者干脆返回Invalid Request

2.2 通知、批量、错误码:容易被忽视的三件套

很多人对 JSON-RPC 的印象就是“一个请求配一个响应”,但规范里还有两个容易被忽视的玩法:通知和批量。

通知就是没有id的请求,比如:

{ "jsonrpc": "2.0", "method": "log/emit", "params": { "level": "info", "message": "agent started" } }

服务端处理完就完事,不返回任何东西。这个机制在 Agent Harness 里特别适合做日志上报、心跳检测、进度通知这类“不需要结果”的调用。注意,如果你发了通知,就不要傻等响应;如果你需要结果,哪怕是空结果,也要带上id发普通请求。

批量请求就是把多个请求对象放进一个数组,一次性发给服务端:

[ {"jsonrpc": "2.0", "method": "fs/read", "params": {"path": "/tmp/a"}, "id": 1}, {"jsonrpc": "2.0", "method": "fs/read", "params": {"path": "/tmp/b"}, "id": 2} ]

服务端会返回一个同样按顺序排列的响应数组。这个能力在 Harness 做“并行工具调用”时很有用,可以一次并行发起多个独立的工具请求,减少网络往返。但要注意,如果这批请求里全是通知,那服务端就不该返回任何东西。

错误码也有标准约定,我直接列个表:

错误码含义
-32700解析错误,收到的文本不是合法 JSON
-32600无效请求,请求对象结构不对
-32601方法不存在,method 没注册
-32602无效参数,params 与预期不符
-32603内部错误,服务端执行时抛异常
-32000 到 -32099服务端自定义错误,预留范围

这个表放在开发文档里很实用。你在 Harness 层排查问题时,只要看错误码就能快速定位是协议层问题、参数问题还是业务执行问题,不用一层层去翻日志。

3. 为什么偏偏是 JSON-RPC,而不是 REST 或 gRPC

3.1 工具调用本质上是 RPC,不是资源增删改查

这是我最想强调的一点。很多人一开口就问:为什么不用 REST?我理解 REST 在大众认知里太成功了,但它和 Agent 工具调用在本质上是有错位的。

REST 的核心是“资源”和“对资源的操作”,你设计的是 URL 和 HTTP 方法,比如GET /users/1POST /orders。但 Agent 的工具调用是“动作”,是“函数”,是“方法调用”。你要的是get_weather(city="杭州"),这在 REST 里该怎么建模?是POST /tools/get_weather,还是GET /weather?city=杭州?如果有一百个工具,你是不是要设计一百套 URL 和参数映射规则?更麻烦的是,很多工具行为并不是简单的资源操作,比如“运行一段代码”“发送一条消息”“查询两个系统并合并结果”,硬套 REST 概念反而别扭。

RPC 的思维模型就完全不同。它天然面向“调用一个远程方法,传参数,拿结果”。而 JSON-RPC 又是 RPC 家族里最轻量的一种:不需要 IDL 文件,不需要代码生成,不需要复杂的序列化框架。你只要会写 JSON,就能实现一个服务端。对于 Agent 这种“工具类型无限多、参数结构千奇百怪”的场景,RPC 风格明显更贴近真实需求。

3.2 和 gRPC 相比,JSON-RPC 的“笨”反而是优势

gRPC 在内部微服务架构里确实很优秀,有强类型约束、有 protobuf 二进制序列化、有 HTTP/2 多路复用,性能也更好。但放到 AI Agent 场景里,它有几个硬伤。

第一条就是:LLM 生成的是文本,生成 JSON 很容易,生成二进制帧几乎不可能。模型的tool_call输出天然是 JSON 结构,如果你想走 gRPC,就得先在代码里把 JSON 转成 protobuf 结构体,这层转换不仅多此一举,还会引入额外的序列化代码和潜在类型错误。JSON-RPC 就不存在这个问题,模型吐出来的 JSON 结构直接就是协议请求体。

第二条是调试和可观测性。JSON-RPC 的请求和响应可以原样打印到日志里,人一眼能看懂,也可以直接复制下来做回放测试。gRPC 的二进制协议要借助额外的工具才能看到内容,在处理 Agent 这种“需要反复看完整调用链”的场景时,体验差得不是一星半点。

第三条是跨语言生态。Agent 的工具端五花八门,有 Python 写的,有 Node 写的,有 Go 写的,甚至可能是一个 Shell 脚本。JSON-RPC 的客户端和服务端在任何语言里都只需要几十行代码就能实现,不需要安装特定的协议生成插件。微型工具可能根本不想引一堆依赖,一个 JSON 解析器就够了。

3.3 JSON-RPC 的短板与 Agent 场景的补法

说了这么多好话,我也得讲讲 JSON-RPC 的短板。它没有内建鉴权,没有内建加密,没有内建服务发现,甚至连“用什么传输层”都没规定。这些在现代分布式系统里都属于基础能力,缺了让人很不安。

但在 Agent Harness 里,这些短板恰恰可以被架构层面自然覆盖。鉴权和权限控制在 Harness 层做,因为 Harness 本身就是控制面;传输层完全自由,本机工具走 stdio,远程工具走 HTTP,实时通信走 WebSocket,协议语义完全一致,这是 REST 和 gRPC 很难做到的灵活性。你看看现在比较火的 MCP,底层也是基于 JSON-RPC 2.0 的,它的做法就是定义了不同的 transport(stdio、HTTP、WebSocket),但方法调用的语义始终统一。这种“一次定义,多传输适用”的特性,是 JSON-RPC 能在 Agent 时代翻红的重要原因。

所以我的结论是:JSON-RPC 2.0 不是没缺点,而是它的缺点在 Agent Harness 的架构里都不致命,它的优点却精准命中了工具调用的核心需求。

4. Agent Harness 里怎么落地 JSON-RPC:一个最小通信层设计

4.1 一次完整工具调用的数据流

理解了协议,接下来要解决的是“在 Harness 里怎么用”。基于我自己的实践,一次典型的工具调用会经历下面这条链路:

  1. Agent 大脑(LLM)在多轮对话里生成了一个tool_call,内容是“调用 get_weather,参数 city=杭州”。
  2. Harness 截获这个tool_call,先做参数校验,检查方法名是否在工具注册表里,参数是否符合 JSON Schema。
  3. 校验通过后,Harness 把tool_call翻译成一个 JSON-RPC 请求,发送给对应的工具服务端。
  4. 工具服务端执行真实动作,返回一个 JSON-RPC 响应。
  5. Harness 收到响应后,把result包装成一条“工具结果消息”,回填给 Agent 大脑。
  6. Agent 大脑根据结果生成最终回复,或者继续发起下一个工具调用。

关键点在第 3 步到第 5 步。Harness 在这里扮演的不只是“转发器”,还是“翻译官”和“安全闸门”。工具服务端根本不需要关心调用它的是一个 LLM 还是一个普通客户端,它只认 JSON-RPC,这让工具可以被不同 Agent 复用的同时,也保证了工具层的绝对独立。

举个例子,我用 Python 写一个极简的 Harness 分发逻辑,大概就是这种感觉:

def dispatch_tool_call(tool_call: dict) -> dict: method = tool_call["name"] arguments = tool_call.get("arguments", {}) # 1. 参数校验,失败则直接返回标准错误 if method not in tool_registry: return {"jsonrpc": "2.0", "error": {"code": -32601, "message": f"tool '{method}' not found"}, "id": tool_call.get("id")} # 2. 构造 JSON-RPC 请求 req = { "jsonrpc": "2.0", "method": f"tools/{method}", "params": arguments, "id": generate_request_id(), } # 3. 发送到工具服务端,等待响应 resp = rpc_call(tool_registry[method].endpoint, req) # 4. 把 resp 返回给上层,最终回填给 LLM return resp

这段代码看着简单,但把 Harness 的职责体现得很完整:校验、翻译、转发、回传。

4.2 id、超时、重试、通知:这几个细节决定健壮性

协议本身很简单,但真正让系统变得健壮的,往往是几个容易被忽视的细节。

id的设计一定要认真。在并发场景里,id是匹配请求和响应的唯一依据。如果每次请求都用固定的id,一旦并发,响应就会串号。我推荐用全局唯一 ID,比如 UUID,或者在进程内用“实例前缀 + 自增序号”,比如agent-1-42。这样即使多个 Harness 实例共用同一个日志系统,也能一眼看出某个请求来自哪个实例、是第几个请求。

超时和重试也必须在 Harness 层做,因为协议本身没有规定。超时时间要根据工具类型区分:文件读取给 5 秒,外部 API 调用给 15 秒,批量数据分析给 60 秒,不要一刀切。重试更有讲究,只有幂等的工具方法才能安全重试,比如“读取文件”“查询天气”可以重试;“发送邮件”“扣款”这类操作重试就要非常谨慎。另外,如果工具的响应里有业务侧的错误码,Harness 可以考虑把错误信息回传给 LLM,让模型自己决定是重试、换参数还是换工具,而不是机械地在代码层重试。

通知模式的使用也要克制。通知适合“不需要结果”的场景,比如日志、指标上报、状态变更广播。很多开发者在刚接触通知时容易手滑,把需要结果的请求也写成通知,结果一调一个不响。我的经验是:默认都用带id的请求,明确知道自己不需要结果时,才用通知。

5. 实操:用 Python 5 分钟搭一个最小示例

5.1 服务端:一个极简 JSON-RPC 工具服务

理论说得再多,不如动手写一遍。我建议你花五分钟跑通一个最小示例,之后再去看任何 Agent 框架的源码,都会觉得亲切很多。下面这个服务端只用 Python 标准库,不依赖 Flask 或 FastAPI,为的是突出“协议无关传输”的概念。

import json from http.server import BaseHTTPRequestHandler, HTTPServer TOOLS = { "get_weather": lambda city, unit="celsius": { "city": city, "temperature": 26 if unit == "celsius" else 79, "unit": unit }, "add": lambda a, b: {"result": a + b}, } class RPCService(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers.get("Content-Length", 0)) payload = json.loads(self.rfile.read(length) or b"{}") # 这里先处理单请求,批量请求留到客户端示例里讲 req_id = payload.get("id") method = payload.get("method") params = payload.get("params", {}) try: fn = TOOLS[method] result = fn(**params) if isinstance(params, dict) else fn(*params) response = {"jsonrpc": "2.0", "result": result, "id": req_id} except KeyError: response = { "jsonrpc": "2.0", "error": {"code": -32601, "message": f"method '{method}' not found"}, "id": req_id, } except Exception as exc: response = { "jsonrpc": "2.0", "error": {"code": -32603, "message": str(exc)}, "id": req_id, } body = json.dumps(response).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, fmt, *args): # 保持控制台干净,真实项目建议把请求和响应都打进系统日志 pass if __name__ == "__main__": HTTPServer(("127.0.0.1", 8080), RPCService).serve_forever()

这段代码的核心思想是:HTTP 只是传输载体之一,协议本身的语义没有绑定任何 HTTP 概念。你换成 WebSocket 或者 stdio,请求和响应对象可以原样复用。

启动服务后,用 curl 测一下:

curl -X POST http://127.0.0.1:8080/rpc \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","method":"get_weather","params":{"city":"杭州"},"id":1}'

返回结果应该类似:

{"jsonrpc": "2.0", "result": {"city": "杭州", "temperature": 26, "unit": "celsius"}, "id": 1}

如果你把method改成一个不存在的名字,就会得到-32601 Method not found。这个错误码就是协议规范里定义的,客户端可以根据它快速判断是“方法名拼错”还是“服务端执行异常”。

5.2 客户端调用与批量请求示例

客户端就更简单了,本质上就是构造 JSON 请求,然后等一个 JSON 响应。我用 Python 的requests演示:

import requests import json def rpc_call(endpoint, method, params=None, req_id=1): payload = {"jsonrpc": "2.0", "method": method, "params": params or {}, "id": req_id} resp = requests.post(endpoint, json=payload, timeout=10) return resp.json() # 单次调用 result = rpc_call("http://127.0.0.1:8080/rpc", "get_weather", {"city": "上海"}, req_id=7) print(result)

批量请求就是发数组:

batch = [ {"jsonrpc": "2.0", "method": "get_weather", "params": {"city": "北京"}, "id": 2}, {"jsonrpc": "2.0", "method": "add", "params": {"a": 1, "b": 2}, "id": 3}, ] resp = requests.post("http://127.0.0.1:8080/rpc", json=batch, timeout=10) print(resp.json())

注意批量请求里每个子请求的id不能重复。响应数组的顺序不一定和请求顺序完全一致,协议不保证这一点,所以一定要根据id来匹配结果,而不是按数组下标。

到这里,你可能已经发现这个最小示例离一个完整 Agent Harness 还差很远,但它已经足够帮助你理解通信层的核心逻辑了。接下来要做的,就是在这个协议之上补充工具注册表、参数校验、权限控制、日志追踪这些 Harness 组件。

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

6.1 Harness runtime 加载失败这类“起不来”的问题

我见过很多朋友在装好 Agent 框架后,启动时直接报错,类似这样:

error: agent harness runtime "codex" is unavailable because its plugin registration failed

看到这个报错先别慌,这类问题一般不在协议层,而在 Harness 的插件机制上。它表达的意思是:Harness 尝试加载名为codex的 runtime,但因为某个插件注册失败,这个 runtime 最终没被启用。常见原因有这么几个:

  • 插件目录或配置文件路径不对,Harness 找不到插件的加载入口。
  • 插件 SDK 版本与 Harness 主程序版本不匹配,注册时接口对不上。
  • 插件依赖缺失,比如某个动态库没装、某个 Python 包没装、某个可执行文件不在 PATH 里。
  • 文件权限问题,Harness 进程没有权限读取插件的配置文件。

排查顺序建议从日志入手。先找 Harness 启动日志里更详细的错误输出,定位具体是哪个插件注册失败;再确认 runtime 名称在配置文件和插件元数据里是否完全一致;接着检查插件依赖的版本和运行环境;最后尝试把插件配置最小化,逐个子插件启用,找到罪魁祸首。

这类问题提示我们,Agent Harness 的插件架构虽然灵活,但也意味着“配置出错”的代价更高。我的经验是,在正式开发前先把插件目录结构和版本约束写进文档,并且用一个启动脚本统一检查环境变量和依赖,能省下不少排错时间。

6.2 调用期容易踩的坑

Harness 能正常启动了,真正的麻烦往往在运行时才出现。我把实际踩过的坑整理成一个速查表:

现象可能原因解决方案
发请求后没有响应请求里漏写id,被当成通知检查请求对象,确保id是数字或字符串,不是 null
params 传数组但工具函数是关键字参数协议参数形态和工具签名不匹配统一在 Harness 层把 params 转成 object 形态
返回-32602无效参数模型生成的参数类型或字段名出错在 Harness 层加 JSON Schema 校验,把错误信息回灌给 LLM 重试
并发响应串号多个请求共用了同一个id使用全局唯一 id,比如 UUID 或实例前缀加自增
工具执行很慢导致客户端超时单个 HTTP 请求超时时间设置太短按工具类型区分超时,或者改用异步通知加回调
返回结果太大,上下文爆炸工具返回长文本、长日志在 Harness 层对结果截断或摘要,再回填给 LLM

其中最值得展开的就是:模型生成的参数不可靠。LLM 在工具调用时,偶尔会把{"city": "杭州"}生成成{"citi": "杭州"},或者把数字26生成成字符串"26"。如果直接把这种请求发给工具服务端,大概率会得到-32602。更聪明的做法是,在 Harness 层把校验失败的错误信息告诉 LLM,让它自己修正参数后重试。

比如你可以向模型返回这样一条消息:

工具 get_weather 调用失败:参数 city 的值为 null,但该字段必填。 请参考以下 JSON Schema 重新生成参数: {"type":"object","properties":{"city":{"type":"string"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["city"]}

这个机制在实践中效果非常明显。LLM 看到具体的错误描述和 Schema 后,大概率能自己修正,成倍提高工具调用的成功率。这是 Agent Harness 特有的“错误反馈闭环”,普通 RPC 客户端不会做这种事。

6.3 几个让 Agent 生成的请求更稳定的经验

最后分享几条实操经验。第一,工具描述里一定要写清楚参数结构,最好直接附 JSON Schema。LLM 并不知道你的工具函数是怎么定义的,它只能从工具描述里学习。描述写得越规范,它生成的arguments就越准确。

第二,不要在 Harness 层无条件信任模型输出。即使是模型生成的调用,也要走一遍参数校验和白名单检查。模型不是每次都会严格遵守约束,harness 就是为了兜底。

第三,日志里一定要保留完整的请求和响应。调试 Agent 的时候,“看得见”比“跑得快”重要得多。我在日志系统里会把tool_call_idmethodparamsresulterror全部结构化记录,这样出了问题可以直接检索和回放。

第四,控制上下文长度。工具返回的结果,有些很长,比如一个文件内容、一段数据库查询结果。直接全部塞回给 LLM,不仅浪费 token,还容易被无关信息干扰。Harness 需要根据场景设置截断长度,或者让模型按需读取“摘要后再去读详情”。

这也算是我整个实践下来最核心的一条心得:Agent 能不能稳定工作,关键不在于模型多聪明,而在于 Harness 这个“外骨骼”够不够稳。而 Harness 的地基,恰恰就是 JSON-RPC 2.0 这套看起来毫不起眼、却极其严谨的老协议。老实说,我刚入这行时觉得 JSON-RPC 很“老土”,现在反而觉得它是大道至简。如果你也在做 Agent 基建,我的建议是别自己发明一套通信协议,直接站在 JSON-RPC 2.0 的肩膀上,把精力花在工具注册、权限控制、错误反馈这些真正影响体验的地方,你会走得更快。

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

从零构建大语言模型:CS336实战路径与迷你GPT实现

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

作者头像 李华
网站建设 2026/9/7 7:42:43

需求是意图,QA是证据:从需求到测试的证据链闭环

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

作者头像 李华
网站建设 2026/9/7 7:39:12

优图房租水电费收据打印软件v11.0:功能详解与zip安装实操

简介:优图房租水电费收据打印软件 v11.0.zip 是一款面向房东、物业及中小企业日常收据管理场景的免安装绿色软件,专注解决房租、押金、水费、电费、燃气费等收据的开具与存档问题。软件采用即输即打设计,无需预先建立出租房资料即可直接开单&…

作者头像 李华
网站建设 2026/9/7 7:39:08

dnSpy实战指南:反编译、调试与修改.NET程序集

简介:dnSpy 是一款面向 .NET 开发者和逆向工程爱好者的 C# 反编译与调试工具,支持将 DLL/EXE 还原为可读的 C# 代码,并集成断点调试、变量查看、热替换及直接修改程序集等能力,适合用于代码学习、问题排查、安全分析及逆向研究。这…

作者头像 李华