跳转至

以下内容基本上是 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
}
  1. 注入 <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)!
  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
  }
}
  1. 删除最旧的条目
  2. Promise resolve 后更新缓存

相关项目

RAG 预处理神器。