Extensions Cf Ai Rag
· · ·
题目进度 0 / 0 ✓ 0
Cloudflare Workers AI + Vectorize 零后端 RAG
杀手锏:用 CF 边缘算力 + 向量数据库,0 业务服务器搭一个 RAG(Retrieval-Augmented Generation)应用。
适用:agquant.com/learning/ 的”语义搜索”——搜”雪球怎么对冲”能匹配到所有相关章节,不只是精确关键词。
一、为什么这是”花活”级别
传统 RAG 架构(自建):
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Frontend │ → │ Backend │ → │ Postgres │
│ (React) │ │ (FastAPI) │ │ + pgvector │
└──────────────┘ │ + LangChain │ └──────────────┘
│ + OpenAI API │
└──────────────┘
成本:
- Frontend hosting: $0-50/月
- Backend VM (4GB): $20-40/月
- Postgres + pgvector: $15-30/月 (RDS) 或自建
- OpenAI Embeddings: $0.02/1M tokens
- OpenAI GPT-4: $30/1M tokens
- 月成本:$60-150
CF 零后端 RAG 架构:
┌──────────────┐
│ 浏览器 │ ← 只用 CF Worker(< 5ms cold start)
│ (静态页面) │
└──────┬───────┘
↓
┌──────────────────────────────────────┐
│ CF Worker (边缘) │
│ - Workers AI: Llama 3 + BGE embed │ ← 全球 200+ 城市节点
│ - Vectorize: 向量数据库 │ ← 一致性 + 边缘缓存
│ - D1: 元数据 + 源文 │ ← SQLite at edge
└──────────────────────────────────────┘
↓
所有数据 + AI 全在 CF,无业务服务器
成本:
- Workers requests: $0.30/1M(10K MAU ≈ 100K queries/月 = $0.03)
- Vectorize: $0.05/1M query-vec(100K = $0.005)
- Vectorize storage: $0.05/GB-月(10K chunks ≈ 100 MB = $0.005)
- D1 reads: $0.001/1K
- D1 writes: $1.00/1M
- 月成本:$0.05-0.50
省 100x。0 业务服务器 = 0 维护。
二、agquant.com 实战:learning 内容语义搜索
2.1 目标
用户在 agquant.com/learning/ 搜:
- “雪球怎么对冲” → 找出所有讲雪球对冲的章节(含同义词)
- “CVA 怎么算” → 找出 CVA 计算公式、Python 实现、风险对冲
- “Elixir GenServer” → 跨 lessons/0002 / 0003 / 0005 的内容
比传统搜索强:用关键词搜索”雪球”能匹配到”雪球结构”和”自动赎回”,但搜”怎么对冲雪球”可能漏掉”Delta 中性策略”。
RAG 解决:用向量相似度,“怎么对冲雪球” 跟”Delta 中性 + 雪球”内容相似度 > 0.85 → 命中。
2.2 数据流
┌─────────────────────────────┐
│ 一次性:建立索引 │
│ (build.mjs 或 CI 里跑) │
└─────────────────────────────┘
↓
learning/otc-derivatives/*.md ───→ 切 chunk(500 字/段)
↓
Workers AI: BGE embeddings
↓
Vectorize index (向量 + metadata)
↓
D1: chunk_text + chunk_id 映射
┌─────────────────────────────┐
│ 运行时:用户搜 │
└─────────────────────────────┘
↓
用户输入 "雪球怎么对冲"
↓
Workers AI: embed (BGE)
↓
Vectorize: query (top 5)
↓
D1: 查 chunk_text
↓
Workers AI: Llama 3.1 8B 拿 chunks 生成答案
↓
返回 {answer, sources[]}
三、完整代码(CF Worker + Vectorize + D1)
3.1 wrangler.toml 配置
# wrangler.toml
name = "agquant-rag"
main = "src/worker.ts"
compatibility_date = "2024-09-01"
[[vectorize]]
binding = "VECTOR_INDEX"
index_name = "agquant-learning"
[[d1_databases]]
binding = "DB"
database_name = "agquant-rag"
database_id = "your-d1-id"
[ai]
binding = "AI"
3.2 worker.ts(核心:embed + query + generate)
// src/worker.ts
export interface Env {
VECTOR_INDEX: VectorizeIndex;
DB: D1Database;
AI: Ai;
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
const url = new URL(req.url);
// 1. 索引 API(一次性,CI 跑)
if (url.pathname === "/api/index" && req.method === "POST") {
return await handleIndex(req, env);
}
// 2. 查询 API(运行时)
if (url.pathname === "/api/search" && req.method === "POST") {
return await handleSearch(req, env);
}
return new Response("Not found", { status: 404 });
},
};
async function handleIndex(req: Request, env: Env): Promise<Response> {
const { chunks } = await req.json<{ chunks: { id: string; text: string; url: string; title: string }[] }>();
// 1. embed 每个 chunk
const embeddings = await env.AI.run(
"@cf/baai/bge-base-en-v1.5", // 768 维向量
{ text: chunks.map(c => c.text) }
);
// 2. 写 Vectorize(带 metadata)
const vectors = chunks.map((chunk, i) => ({
id: chunk.id,
values: embeddings.data[i],
metadata: { url: chunk.url, title: chunk.title }
}));
await env.VECTOR_INDEX.upsert(vectors);
// 3. 写 D1(chunk_text 持久化,Vectorize 不存原文)
const stmt = env.DB.prepare(`
INSERT OR REPLACE INTO chunks (id, url, title, text)
VALUES (?, ?, ?, ?)
`);
const batch = chunks.map(c => stmt.bind(c.id, c.url, c.title, c.text));
await env.DB.batch(batch);
return Response.json({ ok: true, count: chunks.length });
}
async function handleSearch(req: Request, env: Env): Promise<Response> {
const { query } = await req.json<{ query: string }>();
// 1. embed 查询
const queryEmbed = await env.AI.run(
"@cf/baai/bge-base-en-v1.5",
{ text: [query] }
);
// 2. Vectorize 查 top 5
const results = await env.VECTOR_INDEX.query(queryEmbed.data[0], {
topK: 5,
returnMetadata: "all"
});
// 3. 从 D1 拿原文
const ids = results.matches.map(m => m.id);
const placeholders = ids.map(() => "?").join(",");
const { results: rows } = await env.DB.prepare(
`SELECT * FROM chunks WHERE id IN (${placeholders})`
).bind(...ids).all();
// 4. 组装 context
const context = rows.map((r: any) =>
`### ${r.title}\n来源: ${r.url}\n${r.text}`
).join("\n\n---\n\n");
// 5. Workers AI Llama 3 生成答案
const answer = await env.AI.run(
"@cf/meta/llama-3.1-8b-instruct",
{
messages: [
{
role: "system",
content: "你是 agquant.com 学习助手。基于以下 context 回答用户问题。如果 context 没说,诚实说不知道。回答末尾用 [1] [2] 标注引用。"
},
{ role: "user", content: `Context:\n${context}\n\nQuestion: ${query}` }
]
}
);
return Response.json({
answer: answer.response,
sources: results.matches.map(m => ({
title: m.metadata!.title,
url: m.metadata!.url,
score: m.score
}))
});
}
3.3 D1 schema
-- wrangler d1 execute agquant-rag --file=schema.sql
CREATE TABLE IF NOT EXISTS chunks (
id TEXT PRIMARY KEY,
url TEXT NOT NULL,
title TEXT NOT NULL,
text TEXT NOT NULL,
indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_chunks_url ON chunks(url);
3.4 索引脚本(CI 跑)
// scripts/index-learning.ts
import { readdir, readFile } from "fs/promises";
import { join } from "path";
const CHUNK_SIZE = 500;
const OVERLAP = 50;
async function chunkMarkdown(text: string): Promise<string[]> {
const chunks: string[] = [];
// 按段落切(每段最多 500 字,相邻重叠 50)
const paragraphs = text.split(/\n\n+/);
let current = "";
for (const para of paragraphs) {
if (current.length + para.length > CHUNK_SIZE) {
chunks.push(current.trim());
current = current.slice(-OVERLAP) + "\n\n" + para;
} else {
current += "\n\n" + para;
}
}
if (current) chunks.push(current.trim());
return chunks;
}
async function main() {
// 1. 收集所有 .md
const files: string[] = [];
async function walk(dir: string) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) await walk(path);
else if (entry.name.endsWith(".md")) files.push(path);
}
}
await walk("./learning");
// 2. 切 chunk
const allChunks: any[] = [];
for (const file of files) {
const text = await readFile(file, "utf-8");
const chunks = await chunkMarkdown(text);
chunks.forEach((c, i) => {
allChunks.push({
id: `${file}#${i}`,
text: c,
url: `https://agquant.com/learning/${file.replace("./learning/", "").replace(".md", ".html")}`,
title: c.split("\n")[0].replace(/^#+\s*/, "") || file
});
});
}
// 3. 分批调 /api/index(每批 100 个)
for (let i = 0; i < allChunks.length; i += 100) {
const batch = allChunks.slice(i, i + 100);
const res = await fetch("https://agquant-rag.workers.dev/api/index", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chunks: batch })
});
console.log(`Indexed ${i + batch.length}/${allChunks.length}`);
}
}
main();
四、前端嵌入(搜索框 + 流式输出)
<div class="agquant-search">
<input id="q" placeholder="搜索 agquant.com 学习内容...">
<button onclick="search()">🔍</button>
</div>
<div id="result"></div>
<script>
async function search() {
const query = document.getElementById("q").value;
document.getElementById("result").innerHTML = "搜索中...";
const res = await fetch("https://agquant-rag.workers.dev/api/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query })
});
const { answer, sources } = await res.json();
document.getElementById("result").innerHTML = `
<div class="answer">${answer}</div>
<ul class="sources">
${sources.map(s => `
<li>
<a href="${s.url}" target="_blank">${s.title}</a>
<span class="score">${(s.score * 100).toFixed(0)}%</span>
</li>
`).join("")}
</ul>
`;
}
</script>
五、3 个真实使用场景
5.1 语义搜索(替代 Ctrl+F)
用户搜 "雪球怎么对冲"
→ 匹配 5 段:
- product-snowball.html §对冲策略
- 06-otc-options.html §Delta 中性
- 14-Systems-and-Data.html §系统设计
- glossary.html §Delta
- 17-End-to-End-Case.html §案例
5.2 AI 助教
用户问 "我是 IT 转 PM,CVA 怎么入门?"
→ RAG 返回 800 字答案 + 5 个引用链接
→ 末尾 CTA:"想系统学?报名 14 节 OTC PM 课"
5.3 关联内容推荐
用户看完 "雪球结构"
→ 自动推荐:
- "Delta 对冲"(相似度 0.89)
- "波动率曲面"(相似度 0.82)
- "希腊字母"(相似度 0.78)
六、模型选择
| 用途 | 模型 | 维度 | 价格 |
|---|---|---|---|
| 英文 Embedding | @cf/baai/bge-base-en-v1.5 | 768 | $0.02/1M tokens |
| 多语 Embedding(含中文) | @cf/baai/bge-m3 | 1024 | $0.03/1M tokens |
| LLM 8B | @cf/meta/llama-3.1-8b-instruct | - | $0.30/1M tokens |
| LLM 70B | @cf/meta/llama-3.1-70b-instruct | - | $2.00/1M tokens |
| 重排 Rerank | @cf/baai/bge-reranker-base | - | $0.10/1M queries |
agquant.com 推荐:
- Embedding:
bge-m3(中文友好) - LLM:
llama-3.1-8b-instruct(速度 + 成本平衡) - Rerank: 流量大再加
七、跟自建 RAG 对比
| 维度 | CF 零后端 | 自建(LangChain + pgvector) |
|---|---|---|
| 月成本(10K MAU) | $0.05-0.50 | $60-150 |
| 冷启动 | 0ms(边缘) | 100-500ms |
| 维护 | 0 | 自己 |
| 数据主权 | CF 服务器 | 你服务器 |
| 可扩展 | 自动 | 自己 scale |
| 月成本(100K MAU) | $5-20 | $300-1000+ |
| 复杂(Agent + Tool Use) | ⚠️ 有限 | ✅ 完整 LangChain |
何时选自建:
- 需要复杂 Agent(多步推理 + 工具调用 + 反思)
- 数据必须完全自有(金融合规)
- 已有 K8s + DevOps 团队
何时选 CF 零后端:
- 中小流量(< 100K MAU)
- 想要”5 分钟搭好”
- 不愿运维
八、30 分钟搭起来
- 5 分钟:在 CF Dashboard 建 Vectorize index + D1 database + 装 wrangler
- 15 分钟:复制上面 worker.ts,改 env 名
- 5 分钟:
wrangler deploy→ 拿到 worker URL - 3 分钟:跑
scripts/index-learning.ts→ 索引所有内容 - 2 分钟:在 agquant.com 加搜索框 + fetch 调用
30 分钟后你有:全球 200+ 节点的 RAG 搜索,0 业务服务器,月成本 < $1。
九、相关资源
- lesson 0007 Cloudflare 速通 §Workers AI + Vectorize
- CF Vectorize 文档
- CF Workers AI 文档
- BGE 模型
- Llama 3.1 8B Instruct
- 自检清单 → 看下面
十、自检清单
- 知道 RAG = 检索 + LLM 生成,能用自己的话解释
- 知道 CF Worker 跑 Workers AI 和 Vectorize 都是边缘 200+ 节点
- 知道 chunk 切分用 500 字 + 50 字重叠
- 知道 bge-m3 是中英双语 embedding 模型
- 知道 D1 存原文 + Vectorize 存向量的分工
- 知道 0 业务服务器 = 0 维护,CF 全包
- 能在 30 分钟内从零搭好