以下内容基本上是 AI 生成的,我还没校对,可能质量不高
LLM Web Reader¶
基于 LLM 的网页信息提取 demo,去除噪音,输出干净 Markdown。
双层架构¶
项目将正文抽取和 LLM 提取明确分离:先用 @mozilla/readability 做 HTML 清洗,再把清洗后的 HTML 交给流式 LLM 生成 Markdown。
// src/lib/utils/reader.ts
export function read(html: string, baseurl: string) {
const doc = new DOMParser().parseFromString(html, "text/html")
const base = document.createElement("base")
base.href = baseurl
doc.head.appendChild(base) #(1)!
return new Readability(doc).parse()?.content
}
- 注入
<base>标签确保相对资源路径正确
这样分层设计让提取器在边缘或服务器端都能保持一致性。
Edge Runtime + 流式响应¶
API 使用 Edge Runtime 配合 @xsai/stream-text 实现低延迟流式输出:
// src/routes/api/extract/+server.ts
async function extract(html: string) {
const { textStream } = await streamText({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: html },
],
temperature: 0,
apiKey,
baseURL,
fetch: createFetch({ debug: true, retryDelay: 0 }),
})
return textStream
}
export const config = { runtime: "edge" } #(1)!
- 边缘运行时,适合全球部署
自定义 iteratorToStream 将 AsyncIterable 转换为 ReadableStream:
// src/lib/utils/stream.ts
export function iteratorToStream(iterator: AsyncIterable<string>) {
const reader = iterator[Symbol.asyncIterator]();
const encoder = new TextEncoder();
return new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.next();
if (done) {
controller.close();
break;
}
controller.enqueue(encoder.encode(value));
}
},
});
}
高亮缓存¶
shiki 高亮器初始化开销较大,通过 LRU 缓存降低热路径 CPU 消耗:
// src/lib/utils/highlight.ts
export const getHighlighter = cached(async (lang: BundledLanguage) => {
const highlighter = await createHighlighter({ themes: ["vesper"], langs: [lang] });
return cached((code: string) => {
return highlighter.codeToHtml(code, { lang, theme: "vesper" });
});
});
cached 实现了一个简单的 LRU 缓存,默认最多保留 3 个条目:
// src/lib/utils/cache.ts
export function cached<T extends (arg: any) => any>(fn: T, maxSize = 3): (arg: Parameters<T>[0]) => ReturnType<T> {
const cache = new Map<Parameters<T>[0], ReturnType<T>>()
return function (arg: Parameters<T>[0]) {
if (cache.has(arg)) {
return cache.get(arg) as ReturnType<T>
}
if (cache.size >= maxSize) {
const firstKey = cache.keys().next().value
cache.delete(firstKey) #(1)!
}
const result = fn(arg)
cache.set(arg, result)
if (result instanceof Promise) {
result.then((res) => cache.has(res) && cache.set(arg, res)) #(2)!
}
return result
}
}
- 删除最旧的条目
- Promise resolve 后更新缓存
相关项目¶
- 关于 @mozilla/readability,我写了一个 python wrapper:Readability.py
- 和 black.py3.online、markdownify-demo、uv-for-pyodide 用的同一套前端样式。前两个还复用了 py3.online 的 markdown 样式表
RAG 预处理神器。