资讯详情

资讯详情

Wasp TypeScript Spec 实战:用 PG Vector 构建可提问文档库(ask-the-documents 示例全解析)

Wasp TypeScript Spec 实战用 PG Vector 构建可提问文档库ask-the-documents 示例全解析【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp本篇以仓库 examples/ask-the-documents/CLAUDE.md 为主线结合该示例应用的完整源码讲解 Wasp 项目的正确打开方式从main.wasp.ts的 TypeScript Spec 声明、fresh checkout 的标准启动流程、到wasp compile验证体系并深入一条完整的 RAG检索增强生成调用链——抓取网页、生成 Embedding、PG Vector 语义检索、ChatGPT 带引用回答。读完你既能掌握 Wasp 的工程规范也能拿到一份可复刻的文档问答全栈实现方案。CLAUDE.md与AGENTS.md内容一致是一份为 AI 编码 Agent 准备的项目指南但它浓缩了 Wasp 项目的核心工作流文档查找方式、TypeScript Spec 的编码约定、全新检出后的启动顺序、以及验证命令的选择。这些规则不仅对 Agent 有效对任何想要上手 Wasp 的开发者都是最直接的入门地图。下面逐条展开并用 ask-the-documents 的真实源码作为佐证。一、先认识这个示例ask-the-documents 能做什么examples/ask-the-documents/README.md 用一句话概括了它的能力Ask The DocumentsEmbeddings / RAG / ChatGPT一个基于 Wasp 与 PG Vector 的文档问答应用支持抓取整个链接层级非常适合文档站抓取单个链接为页面内容生成 Embedding使用 PG Vector 做语义检索用 OpenAI ChatGPT 与文档对话。应用入口在 src/pages/MainPage.tsx登录后有三个 TabAsk提问、Add Document添加文档URL 树 / 单链接、Search语义搜索。未登录时只展示提问表单——这正好呼应了CLAUDE.md中先设置.env、再启动服务的流程因为提问、搜索、嵌入等操作全部要求登录态。二、main.wasp.tsWasp 版本与应用规格的单一事实来源CLAUDE.md第一组关键约定是main.wasp.ts包含 Wasp 版本和 Wasp 应用规格app spec。当它引用src/中的应用组件/函数时使用with { type: ref }导入。route(name, ...)和crud(name, ...)接受显式名称。其他构造器不接收名称参数声明名就是导入的标识符。例如job(sendReminder, { ... })声明了一个名为sendReminder的 job。wasp.sh/spec由 Wasp 生成不要从 npm 安装。对照 examples/ask-the-documents/main.wasp.ts 可以看到这套规范的完整落地import { action, app, page, query, route } from wasp.sh/spec; import { getGoogleAuthConfig, googleUserSignupFields, } from ./src/auth/google with { type: ref }; import { Layout } from ./src/Layout with { type: ref }; import { Main } from ./src/pages/MainPage with { type: ref }; export default app({ name: askTheDocuments, wasp: { version: 0.26.0 }, title: PG Vector Example, head: [link relicon href/favicon.ico /], auth: { userEntity: User, methods: { google: { userSignupFields: googleUserSignupFields, configFn: getGoogleAuthConfig, }, }, onAuthFailedRedirectTo: /, }, client: { rootComponent: Layout }, server: { envValidationSchema: serverEnvValidation }, spec: [ route(RootRoute, /, page(Main), { prerender: true }), action(embedDocument, { entities: [Document] }), action(getScrapeCandidates, { entities: [Document] }), query(getDocuments, { entities: [Document] }), action(searchDocuments, { entities: [Document] }), action(askDocuments, { entities: [Document] }), action(deleteDocument, { entities: [Document] }), action(deleteAllDocuments, { entities: [Document] }), ], });几个值得展开的细节版本声明wasp: { version: 0.26.0 }是本项目锁定的 Wasp 版本。CLAUDE.md要求据此选择匹配的文档映射docs map并在版本化文档与CLAUDE.md冲突时以文档为准——因为CLAUDE.md可能滞后。ref 导入所有来自src/的组件、函数、实体操作都通过with { type: ref }导入让 Wasp 能识别这是源码里的引用而不是值导入。命名规则route(RootRoute, /, ...)显式传入了路由名而action(embedDocument, ...)、query(getDocuments, ...)没有传名称——操作名就是导入标识符embedDocument/getDocuments。这正是CLAUDE.md强调的其他构造器取声明名。操作声明action/query均带entities: [Document]声明对实体的访问范围searchDocuments、askDocuments、embedDocument被声明为 action写操作getDocuments声明为 query读操作。wasp.sh/spec的来源package.json 中确实写着wasp.sh/spec: file:.wasp/spec——它指向 Wasp 生成的.wasp/spec目录而不是 npm 包。手动从 npm 安装会造成版本漂移。关于 Docs 的查找方式CLAUDE.md建议通过wasp.sh/llms.txt索引找到文档选择与main.wasp.ts中 Wasp 版本匹配的文档映射优先使用文档映射中的 raw Markdown URL。这条建议的工程背景是Wasp 文档按版本归档仓库web/versioned_docs/下从 version-0.11.8 到 version-0.25 有多个版本目录不同版本的 API 与生成物可能不同直接猜wasp.sh/docs的 URL 容易命中错误版本。三、Fresh Checkout从全新克隆到跑起来的正确顺序CLAUDE.md给出了一个严格的启动顺序这个顺序在示例项目里有完整的对应物在全新克隆或 worktree 中先运行wasp install再做其他 Wasp 命令。从示例文件或项目 README 设置.env.server和.env.client。运行wasp db migrate-dev。查看项目 README 确认是否还需要运行 seed。1. wasp install安装依赖wasp install会读取 package.json 安装全部依赖包括heroui/reactUI 组件库、cheerionode-html-markdown网页抓取与 HTML→Markdown 转换、pgvectorPG Vector 的 Prisma 工具函数、openaiEmbedding 与对话模型等。它还会生成.wasp/out下的 Wasp 运行时与.wasp/spec的wasp.sh/spec类型定义——这就是为什么必须在任何其他命令之前先执行它。2. 环境变量.env.server 与 .env.client仓库提供了 .env.server.exampleOPENAI_API_KEYsk-dummy-openai-api-key GOOGLE_CLIENT_IDdummy-g-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRETdummy-g-client-secret复制为.env.server后填入真实值。服务端环境变量校验集中在 src/env.tsimport { defineEnvValidationSchema } from wasp/env; import * as z from zod; export const serverEnvValidation defineEnvValidationSchema( z.object({ OPENAI_API_KEY: z.string({ error: OPENAI_API_KEY is required }), }), );注意虽然.env.server.example列出了三个变量但 schema 只把OPENAI_API_KEY设为必填缺少时启动即报错Google OAuth 的凭据则仅在开启社交登录时需要。该 schema 通过main.wasp.ts的server.envValidationSchema挂载到应用配置上。3. 数据库PG Vector 专用镜像README 中的本地启动命令为wasp start db --db-image pgvector/pgvector:pg18这里特意使用带 pgvector 扩展的 PostgreSQL 镜像因为本项目的数据模型依赖vector类型详见下一节。生产侧对应 fly-db.Dockerfile它在flyio/postgres-flex:17基础上通过apt-get install postgresql-$PG_MAJOR_VERSION-pgvector装上扩展——部署到 Fly.io 时数据库同样具备向量能力。4. 迁移与启动wasp db migrate-dev wasp startwasp db migrate-dev应用 migrations 下的全部迁移后文详述wasp start同时拉起前后端开发服务器。该项目没有 seed 逻辑README 也未要求执行 seed。四、数据模型在 Prisma 中声明向量列schema.prisma 展示了 PG Vector 与 Prisma 的集成方式datasource db { provider postgresql url env(DATABASE_URL) extensions [pgvector(map: vector)] } generator client { provider prisma-client-js previewFeatures [postgresqlExtensions] } model User { id Int id default(autoincrement()) email String? } model Document { id String id default(uuid()) title String url String unique content String embedding Unsupported(vector(1536)) createdAt DateTime default(now()) updatedAt DateTime updatedAt }要点extensions [pgvector(map: vector)]配合previewFeatures [postgresqlExtensions]把 pgvector 的vector类型映射进 Prisma schemaembedding Unsupported(vector(1536))1536 维正好对应 OpenAItext-embedding-3-small的输出维度见 src/documents.ts 中的createEmbeddingPrisma 不直接认识该类型标注为Unsupported代码中通过$queryRaw原生 SQL 读写User模型极简——因为使用的是 Google OAuthgoogleUserSignupFieldssrc/auth/google.ts只把data.profile.email映射为email字段。第一条迁移 migrations/20230907154352_add_extension/migration.sql 揭示了建表时最核心的两件事-- CreateExtension CREATE EXTENSION IF NOT EXISTS vector; -- CreateTable CREATE TABLE Document ( id TEXT NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1536) NOT NULL, CONSTRAINT Document_pkey PRIMARY KEY (id) );CREATE EXTENSION IF NOT EXISTS vector是向量检索能够运行的前提后续迁移add_url、add_social_login、new_auth则逐步补上url唯一约束与新的认证体系。五、核心调用链Embed → Search → Asksrc/documents.ts 是本应用业务逻辑的枢纽它导出的操作与main.wasp.ts的spec一一对应。整套 RAG 管线可以拆成三个环节。5.1 Embedding 入库embedDocumentconst api new openai.OpenAI({ apiKey: env.OPENAI_API_KEY }); export const embedDocument: EmbedDocumentEmbedDocumentInput, EmbedDocumentOutput async (args, { user }) { if (!user) throw new HttpError(401, You must be logged in to embed documents); const { url, selector } args; const { title, markdownContent } await getContent(url, selector); const embedding toSql(await createEmbedding(markdownContent)); await prisma.$queryRaw INSERT INTO Document (id, title, content, embedding, url, updatedAt) VALUES (gen_random_uuid(), ${title}, ${markdownContent}, ${embedding}::vector, ${url}, ${new Date()}) RETURNING id; ; return { success: true }; };流程抓取 URL → 转 Markdown → 调用 OpenAI Embedding 接口 → 用pgvector包的toSql()把向量序列化成 SQL 字面量 → 原生 SQL 插入。embedding通过::vector类型转换写入。getContent来自 src/scrape.ts用ky拉取 HTML、cheerio解析、NodeHtmlMarkdown转 Markdown并内置了一个Map缓存${url}::${selector}作为 key同一 URL选择器组合只抓取一次。5.2 语义检索searchDocumentsexport const searchDocuments: SearchDocumentsSearchDocumentsInput, SearchDocumentsOutput async (args, { user }) { if (!user) throw new HttpError(401, You must be logged in to search documents); const { query } args; const embedding toSql(await createEmbedding(query)); const result (await prisma.$queryRaw SELECT id, title, content, embedding - ${embedding}::vector AS score, ... FROM Document ORDER BY embedding - ${embedding}::vector LIMIT 10; ) as ...; return result.map((result) ({ document: {...}, score: result.score })); };查询文本同样先转为 Embedding再用 PG Vector 的-操作符L2 距离计算余弦近似距离按距离升序取前 10 条返回每条文档及其score。距离越小语义越接近——这是整个语义搜索的数学基础。另外getScrapeCandidates调用getLinksToScrapesrc/scrape.ts解析起始页所有a链接过滤掉带文件扩展名的 URL、跨域链接与锚点 hashcleanUrl去掉#...返回同源链接集合——这就是URL Tree批量抓取的候选列表。5.3 带引用的问答askDocuments最精彩的部分是askDocuments它不只是把相关片段塞给模型而是用OpenAI Function Calling强制模型按结构化 JSON 输出答案 引用来源。将用户问题转成 EmbeddingLIMIT 2取两条最相关文档定义名为answer_with_sources的 function toolschema 要求返回{ answer: string, sources: Array{ part_of_text, url } }其中part_of_text是被用作依据的原文片段url是来源链接构造 system prompt如果答案在文档中不清晰就回答 I dont know最终答案不要包含链接——有效约束模型不要幻觉把检索到的文档原文、按10 / score换算的置信度分值、来源 URL 拼进 system 消息tool_choice强制调用answer_with_sources解析tool_calls[0].function.arguments的 JSON返回{ answer, sources }解析失败则返回兜底文案。前端 src/components/MainPage/AskTheDocumentsForm.tsx 用react-markdown渲染答案并按 URL 对sources分组groupedSources每个来源折叠展示若干条原文摘录点击链接可直接跳到原文——引用可溯源的 RAG 体验由此闭环。六、认证与前端集成src/Layout.tsx 是main.wasp.ts中client.rootComponent指定的根组件基于 HeroUI 的 Navbar未登录显示 Login with GooglegoogleSignInUrl已登录显示 Logout 按钮。Google OAuth 的配置函数src/auth/google.ts声明了scopes: [profile, email]并把profile.email写入用户模型。所有写操作embed/search/ask/delete在服务端都校验user是否存在401 兜底——认证从声明到强制校验贯穿全栈。七、验证体系为什么用 wasp compile 而不是 tscCLAUDE.md的最后一条规则很反直觉但非常关键运行wasp compile检查应用是否有效。不要直接运行tsc做校验。原因在于wasp.sh/spec的导入路径、wasp/server/operations的类型如EmbedDocument、AskDocuments、wasp/entities的实体类型都是在wasp compile或wasp start时由 Wasp 生成到.wasp/out的。直接tsc时这些模块尚未生成或版本不匹配会得到大量假阳性错误。wasp compile会先执行规格校验与代码生成再完成类型检查是唯一可靠的验证入口。e2e 层面e2e-tests/tests/simple.spec.ts 用 Playwright 做了一个冒烟测试打开首页、等待input[typesearch]出现并断言可见——验证应用能成功加载出提问搜索框。运行方式在 package.json 中npm test会先playwright install --with-deps再执行playwright test --config e2e-tests/。值得注意的是该测试在未登录状态下也能通过因为 MainPage 对未登录用户同样渲染提问表单。八、部署形态Fly.io 与带 pgvector 的数据库仓库为部署到 Fly.io 准备了三个文件对应 README 中列出的在线演示地址fly-server.toml应用名为ask-the-documents-server单台 1GB 内存共享 CPU 的虚拟机internal_port 8080强制 HTTPSfly-client.toml静态前端应用配置fly-db.Dockerfile在flyio/postgres-flex:17基础上安装postgresql-$PG_MAJOR_VERSION-pgvector保证生产数据库支持vector列。本地wasp start db --db-image pgvector/pgvector:pg18与生产 Dockerfile 形成镜像式对应无论本地还是云端数据库都必须是PostgreSQL pgvector 扩展的组合这是本项目对运行环境的最核心前提。总结从这份 CLAUDE.md 提炼的 Wasp 开发心法回看 examples/ask-the-documents/CLAUDE.md它其实是一份极简却完备的 Wasp 项目操作手册其规则可以总结为三条主线以main.wasp.ts为锚点版本、应用配置、RPC 声明都集中于此命名遵循route/crud 显式命名、其余取导入标识符的约定wasp.sh/spec一律使用 Wasp 生成版本遵循固定的启动序列wasp install→ 配置.env.server/.env.client→wasp db migrate-dev→wasp start缺一不可且数据库镜像必须携带 pgvector只信任 Wasp 的验证通道用wasp compile代替裸tsc用 Playwright e2e 做冒烟验证。而 ask-the-documents 这个示例恰好把这三条主线落到了实处TypeScript Spec 声明的 7 个操作对应documents.ts的完整 RAG 实现Unsupported(vector(1536))与迁移 SQL 对应 PG Vector 的落地-距离检索 Function Calling 构成了可溯源引用的问答体验。把它当作模板你可以在几分钟内复刻出一套属于自己的文档问答全栈应用。【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →