言外 · 技术文档
一个运行在知乎网页上的划词解析工具。四个方向并行检索资料,再由综合器写成一段解析,帮用户看懂看不懂的内容。
1项目概述
一个运行在知乎网页上的划词解析工具,帮用户看懂看不懂的内容。
核心功能:
- 用户在知乎页面选中任意文本(问题、回答、评论)后,弹出悬浮气泡。
- 气泡内调用 AI,从四个角度解析选中的文本:术语解释(其中的专业名词或黑话)、热点关联(相关的热点事件或历史背景)、潜台词解读(结合上下文,解释这段话的隐含意思、梗或阴阳怪气)、社区理解(AI 自身信息不足时,结合知乎上其他用户的讨论与解读,给出更贴近真实用户视角的解释)。
- 气泡底部提供输入框,用户可以补充自己的理解或要求换个角度解释,AI 重新生成解析。
- 界面简洁,参考知乎蓝白配色,气泡不遮挡正文。
知乎的内容门槛不在字面,在背景。一段话里可能同时藏着专业术语、几年前的站内旧事、一个只有圈内人懂的梗,以及一层阴阳怪气。看得懂每个字,却不知道这段话在说什么、在骂谁、在维护谁。而看不懂的人,恰恰是最没有能力提问的人。所以言外不做问 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)共用同一套核心,差异只收敛在站点适配器。
- 后端编排层: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一次解析的旅程
两段式体验:一句话人话不依赖任何证据,永远最先到(≈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/ # 管理后台
模块按可插拔原则组织。实际落地的是三组接口,加一个方向或换一个模型都只改配置、不改主流程:
| 接口 | 形状 | 换掉它要做什么 |
|---|---|---|
Direction | id / name / sources(资料源列表) / retrieve() / distill() | 加一个检索方向:写模块 + 在 config.directions 注册 |
DataSource | id / 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 字,按要让读者读懂最少需要交代几件事在区间内取值,不是每篇都写着一样长。
红线写进提示词系统前缀,对所有输出生效:
- 证据不足就说不知道,绝不编造事件 / 时间 / 人名 / 数据
- 有料的方向覆盖,没料的方向绝口不提
- 引用标注:正文的引用标记指向注释区的来源条目,后端校验越界即剥除,防止编出不存在的引用
- 没站内证据时明说站内相关讨论不多,不写成知乎用户普遍认为