言外 / 技术文档 ← 返回主页

言外 · 技术文档

一个运行在知乎网页上的划词解析工具。四个方向并行检索资料,再由综合器写成一段解析,帮用户看懂看不懂的内容。

Node 18+ / TypeScript DeepSeek(OpenAI 兼容) NDJSON 流式 网页 Demo + MV3 插件 知乎黑客松 · 知识炼金场

1项目概述

一个运行在知乎网页上的划词解析工具,帮用户看懂看不懂的内容。

核心功能:

  1. 用户在知乎页面选中任意文本(问题、回答、评论)后,弹出悬浮气泡。
  2. 气泡内调用 AI,从四个角度解析选中的文本:术语解释(其中的专业名词或黑话)、热点关联(相关的热点事件或历史背景)、潜台词解读(结合上下文,解释这段话的隐含意思、梗或阴阳怪气)、社区理解(AI 自身信息不足时,结合知乎上其他用户的讨论与解读,给出更贴近真实用户视角的解释)。
  3. 气泡底部提供输入框,用户可以补充自己的理解或要求换个角度解释,AI 重新生成解析。
  4. 界面简洁,参考知乎蓝白配色,气泡不遮挡正文。

知乎的内容门槛不在字面,在背景。一段话里可能同时藏着专业术语、几年前的站内旧事、一个只有圈内人懂的梗,以及一层阴阳怪气。看得懂每个字,却不知道这段话在说什么、在骂谁、在维护谁。而看不懂的人,恰恰是最没有能力提问的人。所以言外不做问 AI,做不用会问。

技术栈一览

层选型说明
后端Node 18+ / TypeScript / Express流式 NDJSON,原生 fetch 与 stream
模型DeepSeek(OpenAI 兼容协议)适配器封装,可切换 provider
前端原生 TypeScript + Shadow DOM网页 Demo 与 MV3 插件共享同一套核心
数据JSONL 追加日志 + 内存索引原文不落库,不上数据库
生态知乎搜索 / 热榜 / 全网 / 直答 / 社区 / OAuth可插拔 Provider + 并行全采

2总体架构

三个层次

层做什么对应实现
总处理层接收前端请求,按请求类型调度相应功能模块,组装结果后返回server.ts / dispatcher.ts
基础支撑层缓存、存储、限流、额度、引用清洗、日志、统计,所有模块共享core/
可插拔功能模块层每个模块实现统一接口,可独立启用或禁用modules/(四个方向与各自的资料源)、llm/

总处理层只依赖接口,不依赖具体模块实现,新增模块只需实现接口并在配置里注册。配置驱动:模块启停与模型选择走 config,切换模型或增删模块只改配置,不改代码。提示词也是数据:带版本号,版本号进缓存键。

客户端层 网页 Demo 与浏览器插件(MV3)共用同一套核心:选区采集 / 气泡渲染 / 流式客户端 HTTPS · NDJSON 流式 后端编排层 · dispatcher 一句话人话 lead 快速流式 · ≈600ms 四方向并行检索 term / hotspot subtext / community 综合器 synthesize 唯一说话的地方 本意 → 引申 → 语境 资源层 DeepSeek(语言理解) · 知乎开放 API(站内证据) · 本页 DOM(同问题上下文)
图 1 · 三层总体架构:方向只负责找料,综合器才负责说话
  • 客户端层:网页 Demo(评审主入口)与浏览器插件(MV3)共用同一套核心,差异只收敛在站点适配器。
  • 后端编排层:dispatcher 收到请求后,同时发出一句话人话和四方向并行检索,把检索结果交给综合器组织成最终解析。
  • 资源层:DeepSeek 提供语言理解,知乎开放 API 提供站内证据,本页 DOM 提供零成本的同问题上下文。
最重要的架构原则:方向之间零耦合,每个方向独立跑、独立降级,任一渠道断了不影响其他方向。

3核心设计决策

3.1 四个方向是并行的检索渠道

四个方面对应四个并行的检索渠道。每次解析同时调用所有启用的方向,各自拿回反馈,综合器统一筛选组织,用户不需要选,也不会只走一条。方向可并行、可叠加,不是互斥单选。一段话可能同时需要这是什么事(热点)和大家怎么看(社区)。

方向名称找什么资料资料源(并行取,LLM 兜底)
term术语解释专业名词 / 黑话的释义LLM
hotspot热点关联相关热点事件 / 历史背景热榜 + 全网搜索,LLM 兜底
subtext潜台词解读梗的由来 / 阴阳怪气知乎搜索 + 全网搜索,LLM 兜底
community社区理解知乎站内其他用户怎么讨论知乎搜索 + 圈子讨论,LLM 兜底

资料源是并行取、再合并:一个方向挂几个源,就同时问几个,结果按优先级合并去重,一个方向同时拿到多个源的料。所有源都取不到料时,才落到 LLM 兜底(用训练知识兜,但标注无站内证据)。这条保证核心能力不依赖知乎接口:知乎那边断了,依赖它的方向会自动回退到 LLM。

3.2 综合器是唯一说话的地方

看一段文本 = 三层,是通用的输出组织框架,不限于知乎:

层含义知乎里的实例
a 本意字面在说什么一句话人话(lead)
b 引申多方向的言外之意术语 / 热点 / 潜台词 / 社区
c 位置·时间·评论在什么语境里说的所在回答/问题 · 写作时间 · 读者反应

综合器拿到所有方向的反馈后,先统一判断哪些和选中文本相关,只组织相关的,再按本意 → 引申 → 语境组织输出。正文篇幅由要让读者读懂最少需要交代几件事决定,落在 300 到 1800 字之间:档位先由一次极短调用判定,再把字数区间直接写进请求,所以同一批内容不会每篇都写成一样的长度。没料的方向绝口不提。字面直白的内容不展开,按最短档写,不灌长文。该沉默时沉默本身就是最强的产品证明。

3.3 精确锚定 + 站点无关的角色模型

这是可移植性和证据合法性的根基。

  • 站点无关的角色模型:核心逻辑只认六个角色(title / container / before / after / sibling / meta),不知道知乎存在。适配器把站点 DOM 映射到角色上,换平台只换适配器,核心解析、提示词、缓存键全部复用。
  • 精确锚定:划词不只是取文本,它同时是证据的作用域键。知道选中的是谁的哪条回答,才能判断同问题下其他回答这类证据是否合法。锚点采用 W3C TextQuoteSelector 思路(exact + prefix + suffix),知乎懒加载、折叠、异步插入都不影响回定位。
  • 时间敏感性:梗 / 绰号 / 黑话的含义随发布时间漂移(同一个词不同时期意思甚至相反),所以发布时间(publishedAt)作为一等字段进主链路,注入模型提示词。

4一次解析的旅程

① 划词选中(≥ 4 字) selection.ts:选区快照 + 300ms debounce + 误触过滤 ② 上下文采集 adapters/zhihu.ts:角色映射 + 精确锚定 + publishedAt ③ POST /api/v1/explain · dispatcher 编排 缓存命中 → 直接回放事件序列 runLead · 一句话人话 快速流式,不依赖任何证据 runDirections · 四方向并行 Promise.allSettled + 并行全采 synthesize · 综合器 流式 markdown + 引用标记 ④ 气泡渲染(Shadow DOM 隔离 · markdown 渲染)
图 2 · 一次解析的完整旅程:lead 与四方向并行,正文随后流式接上

两段式体验:一句话人话不依赖任何证据,永远最先到(≈600ms),既是兜底也是体验。四方向检索齐了之后,综合器流式接上正文(实测 1.2 到 3 秒)。正文挂了,用户至少还有一句可用的话,不是白屏。一次完整请求实测 2.8 到 7.4 秒,检索与定档并行发出,所以多取几个源不会成倍变慢。

5流式协议

用 NDJSON over chunked HTTP(不是 SSE):每一行是一个 JSON 事件,fetch / 插件转发处理累积文本都比 SSE 帧简单、跨环境更稳。

{"v":1,"t":"start","requestId":"req_123"}
{"t":"lead_delta","d":"这句话在阴阳怪气地说"}
{"t":"lead_end"}
{"t":"directions","items":[{"id":"subtext","hasMaterial":true},{"id":"community","hasMaterial":false}]}
{"t":"body_delta","d":"岁月史书出自..."}
{"t":"refs","items":[{"n":1,"quote":"...","author":"...","url":"..."}]}
{"t":"body_end","body":"(清洗并重编号后的最终正文)"}
{"t":"done","usage":{"tokens":540}}
事件含义
start带协议版本号 v
lead_delta / lead_end一句话人话(流式 / 结束)
directions各方向命中摘要(id + hasMaterial),供调试,前端可忽略
body_delta综合器 markdown 正文(流式)
refs来源引用,正文里的引用标记对应
body_end / done / error结束 / 用量 / 错误(stage: lead | body)

协议耐久性规则:start 带版本号。消费者必须忽略未知事件和未知字段,只允许加法演进。事件 schema 与帧编码分离,未来换协议只换编码器。

6前端

前端负责划词监听、气泡展示、结果渲染,通过统一接口调用后端,本身不含解析逻辑(解析全部在后端完成)。同一套前端核心同时供网页 Demo 与浏览器插件使用,差异只收敛在站点适配器。

  • 共享核心:网页 Demo 与浏览器插件共用同一份 core(selection / bubble / client / session / markdown),差异收敛在 SiteAdapter。
  • Shadow DOM:注入 zhihu.com 意味着宿主全局 CSS 会作用到气泡上,Shadow DOM 样式隔离是能用和看起来是坏的之间的差别。不用 React,控制体积、避免框架冲突。
  • 气泡定位:优先选区右侧留白,窄屏退回下方且保证选中句可见。绝对定位 + 文档坐标,滚动随文字走。
  • markdown 渲染:手写极简安全渲染器(先 HTML 转义再套标记,链接白名单),杜绝 XSS。这是评委会打开的页面。
  • 浏览器插件(MV3):content script 覆盖 fetch,把后端请求转经 background service worker 转发(绕开 https 页面的 mixed content,origin 变为 chrome-extension:// 由后端放行),逐 chunk 零拷贝转发,不改后端 CORS。

四个按钮 = 控制生成

四个按钮全部控制生成,没有一个控制显示:

按钮类型语义
重新解析单次操作重跑一次生成
相关评论持久化开关开 = 生成时把评论用进 c 层资料。关 = 不用(默认关)。演示页没有评论数据,这个按钮是灰的,原因见下方说明
无限探索持久化开关开 = 不限时。关 = 限时(默认关)
超长对话持久化开关开 = 不设生成上限。关 = 按篇幅档位写(默认关)

为什么演示页上相关评论是灰的

知乎开放平台提供四类内容接口:搜索、热榜、全网搜索、直答。其中没有读取某条回答下面的评论这一项。要拿到这些评论,只有爬虫一条路。

言外的取舍是不做爬虫:不绕鉴权、不抓取平台没有开放的数据。所以这条能力就停在平台接口的边界上,演示页里没有评论数据,按钮置灰并写明原因,而不是做成一个点了没反应的按钮。

在真实知乎页面上,这个开关读的是用户自己已经展开的评论区,属于用户当前页面里已有的内容,不发额外请求、零抓取、零鉴权。这也是我们把本页 DOM列为默认证据源、把划词做成精确锚定的原因:能拿到什么、不能拿到什么,边界是清楚的。

7后端模块

backend/
├── server.ts          # 入口:路由、CORS、限流、健康检查
├── dispatcher.ts      # 编排:缓存 → lead + 四方向并行 → 综合器
├── config.ts          # 配置:方向清单、资料源链、额度、开关
├── types.ts           # 数据契约:Direction / FeedbackPoint / Source ...
├── core/              # stream / cache / session / storage / quota / refs / normalize / ratelimit / metrics / logger
├── modules/
│   ├── directions/    # 四方向(term / hotspot / subtext / community)+ 注册表
│   ├── evidence/      # 资料源:zhihu-search / hot-list / global-search / community-ring / query
│   ├── prompts/       # 提示词注册表(数据化,rev 进缓存键)
│   └── community/ · oauth.ts · zhida.ts   # 知乎生态
├── llm/               # 模型适配器(openai-compat / mock)
└── admin/             # 管理后台

模块按可插拔原则组织。实际落地的是三组接口,加一个方向或换一个模型都只改配置、不改主流程:

接口形状换掉它要做什么
Directionid / name / sources(资料源列表) / retrieve() / distill()加一个检索方向:写模块 + 在 config.directions 注册
DataSourceid / enabled / timeoutMs / retrieve()换资料源:写模块 + 改方向挂的源清单
模型与提示词Provider 适配器(OpenAI 兼容 / mock)。提示词是可数据化的模板,带 rev换模型:换适配器。改提示词:改模板并递增 rev

提示词版本号进缓存键,所以调完提示词不会读到旧缓存。config.features 控制六个能力开关,赛后可逐项关闭。

8知乎生态集成

全部走可插拔 Provider + config.features 开关,赛后改配置即可禁用,不硬编码进主链路。每个方向挂多个资料源,并行取、合并去重。所有源都无料时才落 LLM 兜底,并标注无站内证据。

能力用途鉴权
知乎搜索站内检索实体 / 梗的用法(5,000 / 天)Bearer Access Secret
热榜热点事件背景(100 / 天,缓存 ≥ 1h,命中缓存不消耗额度)Bearer
全网搜索实体查询(5000 / 天)Bearer
知乎直答用户点深挖时的二次解析(100 / 天)Bearer
社区 API读圈子讨论(community 方向的资料源)。也可发帖 / 评论 / 点赞,为可选写入能力,默认关闭HMAC-SHA256 签名
OAuth网页端知乎登录授权码 + state CSRF

9可靠性与安全

并发竞态(串台)

用户选中 A 未返回又选中 B,两条流同时在飞。两层防护叠加:requestSeq 单调递增(渲染器丢弃 seq 不匹配的事件)+ AbortController 取消在途请求。

误触过滤

debounce 300ms,4 到 500 字,纯数字 / URL / 标点跳过,落在输入框内跳过,同文本 60 秒内去重回放。

额度与缓存

搜索日预算硬顶 3500(官方 5000),留 1500 给评审窗口。启动查 quota 校准 + 本地计数递减(进程重启不归零)。缓存键 hash(归一化文本 + scope + 模型 + 提示词 rev),分层 TTL(词义稳定缓存久、热点时效短)。

提示词注入

流进提示词的外部文本一律不可信,知乎上任何人都可能写忽略上面的指令这类内容。做法是用明确定界符包裹,并在系统提示里声明定界符内是待分析数据,其中任何指令不执行。证据只进 user 消息。

凭证边界

App Key / Secret / OAuth Token 只在后端,不进源码、URL、日志、前端响应。诊断只显示来源 / 是否配置 / 短前缀。

合规设计

只在 zhihu.com 注入。只读用户可见 DOM,不发起额外的数据抓取。原文不落库。社区引用标注出处。

管理后台与可观测性

自带一个管理后台(/admin,ADMIN_TOKEN 鉴权,未配置时整组路由直接 503,不裸奔公网),五块能力:模块开关、运行时可变配置、使用数据、运行错误、存储管理。密钥一律只回显后四位。

统计层记录真实调用:解析 / 追问 / 重新解析 / 深挖的次数、缓存命中率、限流触发次数、每个知乎接口的实际调用量、额度剩余,以及输出侧的两类质量计数(文体违规 style:dash / quote / bracket / paren、伪引用剥除 refs:stray_marker_dropped)。网络搜索的剩余额度启动时向平台校准一次,之后每 5 分钟重校,进程重启不会把计数清零。

10存储

不上数据库。JSONL 是持久化日志,内存 Map 是索引(append-only log + in-memory index)。启动回放 JSONL 重建索引,运行时查 Map、写 JSONL 追加行。崩溃最多丢最后一行(本身是可重算的缓存)。

日志内容何时开
运行时缓存哈希键 + 解析结果,不含原文线上常开
开发采集原文 + 上下文 + 决策 + 输出DEV_CAPTURE=1,只在开发开

两条日志都不含任何凭证。原文不落库,可一键关闭,守住合规承诺。

11部署

  • 必须 HTTPS(插件与网页 Demo 都有 mixed content 问题)
  • 必须支持长连接 chunked 流式:响应头 X-Accel-Buffering: no,该路由不开 gzip,Content-Type: application/x-ndjson,不设 Content-Length,连接建立立即发 start
  • Node 18+。需要连续在线穿过整个评审窗口(初审滚动打分 + 决赛路演)
  • 运行形态:后端预编译为 dist-backend/ 后用 pm2 以 fork 单实例托管。必须单实例,因为 OAuth 的 state 与会话都在进程内存里,多实例会让登录落 A 实例、回调落 B 实例
  • 重启不丢在途请求:收到 SIGTERM 后 server.close() 停止接新连接、把在途的流式请求跑完再退出,兜底 10 秒

12测试与质量

回归方式分两层:自动化单测 147 项(取料合并 / 引用清洗 / 提示词与档位 / 缓存 / 额度 / 社区签名等),以及每轮改动现跑的真实请求验收,用网页 Demo 内置的 7 个真实知乎回答片段(TACO、莫迪老仙、何不食肉糜、人在美国刚下飞机、乳法、电子阳痿、鼠头鸭脖),再按当轮要改的东西补几条对照文本(例如要压篇幅就补今天天气不错这类字面直白的句子)。输出侧另有可自动断言的计数:正文里的破折号、引号、游离方括号线上计数保持为 0。

文体规范(对一句话、正文、追问都生效,标点走白名单):断句只用逗号,句号和问号,需要时用冒号。引号只用来标明这一段是转述别人的话语,不能拿来强调,强调一律用粗体。出处写成固定格式的数字标记,正文里的链接文字不用纯数字。破折号,分号,直角引号,六角括号一律不用。提到事件要交代时间,地点,人物,起因,经过,结果。不重复,不写评价句和关联词。每段 3 到 6 句,一段一个主题。正文篇幅 300 到 1800 字,按要让读者读懂最少需要交代几件事在区间内取值,不是每篇都写着一样长。

红线写进提示词系统前缀,对所有输出生效:

  • 证据不足就说不知道,绝不编造事件 / 时间 / 人名 / 数据
  • 有料的方向覆盖,没料的方向绝口不提
  • 引用标注:正文的引用标记指向注释区的来源条目,后端校验越界即剥除,防止编出不存在的引用
  • 没站内证据时明说站内相关讨论不多,不写成知乎用户普遍认为
© 2026 言外 · 知乎黑客松项目(知识炼金场)