从源码看 WeKnora 的技术栈:它不是简单的 RAG Demo

基于源码梳理 WeKnora 的主后端、前端、文档解析、默认检索、向量库适配、模型层和 Agent 扩展能力,判断它更像企业知识库底座,而不是轻量级 RAG Demo。

文章目录

最近我在调研腾讯开源的 WeKnora,目标不是单纯跑起来看效果,而是判断它能不能作为企业内部知识库和 PRD 生成助手的底座。

一开始我以为它可能只是一个“Go 后端 + Vue 前端 + 向量库”的 RAG 项目。真正看完源码后发现,它的结构比普通 RAG Demo 重很多:主后端、前端、文档解析服务、向量检索适配层、Agent、MCP、Skills、IM 接入、飞书/语雀/Notion 同步、Langfuse 可观测都已经有了。问题也很明确:底座能力很完整,但如果要做垂直场景,比如“基于历史 PRD 自动生成新 PRD”,还需要继续二次开发业务流程。

项目整体判断

WeKnora 的核心定位不是“一个问答接口”,而是一个企业知识库平台。

它的源码大致可以拆成几层:

层级技术栈
主后端Go、Gin、GORM、dig、Viper、JWT
前端Vue 3、TypeScript、Vite、Pinia、TDesign
文档解析Python、gRPC、MarkItDown、OpenDataLoader、LibreOffice、Playwright
默认数据库PostgreSQL / ParadeDB
默认向量检索pgvector、halfvec、HNSW
关键词检索ParadeDB
异步任务Redis、Asynq
模型接入OpenAI、DeepSeek、DashScope、智谱、Gemini、混元、Ollama 等
Agent 能力ReAct Agent、MCP、Skills、Sandbox、审批机制
数据源同步飞书、语雀、Notion、RSS
IM 接入企业微信、飞书、Slack、Telegram、钉钉等
可观测Langfuse、ClickHouse、MinIO
部署Docker Compose、Nginx、Makefile

这套架构明显是按企业私有化部署来设计的,不是只为了本地体验。

后端:Go 是主干,Gin 负责 HTTP 服务

主服务入口在:

cmd/server/main.go

里面可以看到 Gin 的使用方式很直接:

if os.Getenv("GIN_MODE") == "release" {
    gin.SetMode(gin.ReleaseMode)
} else {
    gin.SetMode(gin.DebugMode)
}

c := container.BuildContainer(runtime.GetContainer())

err := c.Invoke(func(
    cfg *config.Config,
    router *gin.Engine,
) error {
    server := &http.Server{
        Handler: router,
    }

    addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
    return server.ListenAndServe()
})

真正重要的不是 Gin 本身,而是它用了 go.uber.org/dig 做依赖注入。大部分组件都在 internal/container/container.go 里装配,包括:

must(container.Provide(config.LoadConfig))
must(container.Provide(initDatabase))
must(container.Provide(initRedisClient))
must(container.Provide(initDocReaderClient))
must(container.Provide(initOllamaService))
must(container.Provide(initNeo4jClient))
must(container.Provide(repository.NewKnowledgeRepository))
must(container.Provide(service.NewKnowledgeService))
must(container.Provide(service.NewAgentService))
must(container.Provide(router.NewRouter))

这个设计说明项目不是简单 Controller 调 Service,而是把基础设施、Repository、Service、Handler、Agent、MCP、IM、Pipeline 全部通过容器统一管理。

好处是扩展点清晰,坏处是新手读源码时不容易顺着调用链一路跟下去。

前端:Vue 3 + Vite + TDesign

前端在 frontend/ 目录,package.json 里能看到核心依赖:

{
  "dependencies": {
    "vue": "^3.5.34",
    "vue-router": "^4.5.0",
    "pinia": "^3.0.4",
    "tdesign-vue-next": "^1.19.2",
    "axios": "^1.16.0",
    "marked": "^17.0.5",
    "mermaid": "^11.15.0",
    "katex": "^0.16.45",
    "docx-preview": "^0.3.7",
    "@vue-office/pptx": "^1.0.1"
  },
  "devDependencies": {
    "vite": "^7.2.2",
    "typescript": "~6.0.3",
    "vue-tsc": "^3.2.8"
  }
}

前端不是 Node 服务常驻运行,生产环境是构建成静态资源后交给 Nginx:

FROM nginx:stable-alpine

COPY dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/templates/default.conf.template

EXPOSE 80

所以它的前端定位很明确:管理后台、知识库配置、会话页面、文档预览、模型配置、数据源配置。

如果后面我要加“PRD 生成页面”,主要工作就在 frontend/src/ 里新增页面、API 调用、生成结果预览和配置项。

文档解析:单独拆了 Python docreader 服务

WeKnora 没有把复杂文档解析写在 Go 里,而是单独做了一个 Python 服务 docreader

部署上,它是一个独立容器:

docreader:
  image: wechatopenai/weknora-docreader:${WEKNORA_VERSION:-latest}
  expose:
    - "50051"
  environment:
    - DOCREADER_IMAGE_OUTPUT_DIR=/tmp/docreader
    - DOCREADER_PDF_RENDER_DPI=${DOCREADER_PDF_RENDER_DPI:-200}

Go App 通过 gRPC 调它:

DOCREADER_ADDR=${DOCREADER_ADDR:-docreader:50051}
DOCREADER_TRANSPORT=${DOCREADER_TRANSPORT:-grpc}

Python 依赖里有这些关键库:

dependencies = [
    "grpcio>=1.78.0",
    "markitdown[docx,pdf,xls,xlsx]>=0.1.3",
    "opendataloader-pdf>=2.4.7",
    "pypdf>=6.1.3",
    "pypdfium2>=5.8.0",
    "python-docx>=1.2.0",
    "openpyxl>=3.1.0",
    "playwright>=1.55.0",
    "trafilatura>=2.0.0",
    "textract==1.5.0"
]

这个拆法比较合理。文档解析天然依赖 Python 生态,尤其是 PDF、Office、网页正文提取、图片渲染这些能力。如果硬塞到 Go 里,工程复杂度会更高。

文档解析服务返回的核心内容是:

文件 / URL
  -> Markdown content
  -> image refs
  -> metadata

也就是说,知识库入库前会先被统一转换成 Markdown,再进行 chunk、embedding、索引写入。

默认检索:PostgreSQL + ParadeDB + pgvector

WeKnora 默认数据库不是普通 PostgreSQL,而是用了 ParadeDB:

postgres:
  image: paradedb/paradedb:v0.22.2-pg17

这个选择很关键。它同时解决两个问题:

能力实现
关键词检索ParadeDB
向量检索pgvector
业务数据存储PostgreSQL

源码里 PostgreSQL retriever 支持两种检索:

func (r *pgRepository) Support() []types.RetrieverType {
    return []types.RetrieverType{
        types.KeywordsRetrieverType,
        types.VectorRetrieverType,
    }
}

关键词检索用的是 ParadeDB 的语法:

conds = append(conds, clause.Expr{
    SQL:  "content ||| ?",
    Vars: []interface{}{params.Query},
})

向量检索用的是 pgvector,并且用了 halfvec 和 HNSW:

queryVector := pgvector.NewHalfVector(params.Embedding)

querySQL := fmt.Sprintf(`
    SELECT id, content, source_id, chunk_id,
           (1 - distance) as score
    FROM (
        SELECT id, content, source_id, chunk_id,
               embedding::halfvec(%[1]d) <=> $1::halfvec(%[1]d) as distance
        FROM embeddings
        %[2]s
        ORDER BY embedding::halfvec(%[1]d) <=> $1::halfvec(%[1]d)
        LIMIT $%[3]d
    ) AS candidates
    WHERE distance <= $%[4]d
    ORDER BY distance ASC
    LIMIT $%[5]d
`, dimension, whereClause, subqueryLimitParam, thresholdParam, finalLimitParam)

这里有两个值得注意的点:

  1. embedding::halfvec(dim) 不是多余的转换,而是为了匹配 HNSW 表达式索引。
  2. expandedTopK 被限制在 100 到 200 之间,是为了避免 HNSW 查询退化成接近全表扫描。

这说明 WeKnora 在默认检索链路上已经不是玩具实现,而是考虑了实际性能问题。

向量库适配:不是绑定 pgvector

虽然默认推荐 PostgreSQL / ParadeDB / pgvector,但源码里抽象了向量库引擎层。

internal/container/engine_factory.go 里根据不同类型创建检索引擎:

switch store.EngineType {
case types.PostgresRetrieverEngineType:
    return createPostgresEngine(store, db)
case types.ElasticsearchRetrieverEngineType:
    return createElasticsearchEngine(store, cfg)
case types.QdrantRetrieverEngineType:
    return createQdrantEngine(store)
case types.MilvusRetrieverEngineType:
    return createMilvusEngine(ctx, store)
case types.WeaviateRetrieverEngineType:
    return createWeaviateEngine(store)
case types.DorisRetrieverEngineType:
    return createDorisEngine(store)
case types.SQLiteRetrieverEngineType:
    return createSQLiteEngine(store, db)
case types.TencentVectorDBRetrieverEngineType:
    return createTencentVectorDBEngine(store)
case types.OpenSearchRetrieverEngineType:
    return createOpenSearchEngine(ctx, store, auditSink)
}

也就是说,它支持:

向量库适合场景
PostgreSQL / pgvector默认部署、简单可靠、业务数据和向量放一起
SQLite vectorLite 模式、本地单机体验
Qdrant专用向量库,部署复杂度适中
Milvus大规模向量检索
Weaviate向量检索 + 对象模型
Elasticsearch / OpenSearch关键词和向量混合检索
Doris分析型场景、大规模数据
Tencent VectorDB腾讯云体系

对我这种想先本地跑通、后面再考虑企业部署的人来说,默认 PostgreSQL / ParadeDB / pgvector 是最省事的选择。除非数据规模或检索性能到了瓶颈,否则没必要一开始就换 Milvus、Qdrant。

模型层:按能力抽象,而不是写死某个厂商

模型类型在源码里分为:

const (
    ModelTypeEmbedding   ModelType = "Embedding"
    ModelTypeRerank      ModelType = "Rerank"
    ModelTypeKnowledgeQA ModelType = "KnowledgeQA"
    ModelTypeVLLM        ModelType = "VLLM"
    ModelTypeASR         ModelType = "ASR"
)

服务接口也是按能力获取模型:

GetEmbeddingModel(ctx, modelId)
GetRerankModel(ctx, modelId)
GetChatModel(ctx, modelId)
GetVLMModel(ctx, modelId)
GetASRModel(ctx, modelId)

Provider 层支持很多模型服务商:

ProviderOpenAI
ProviderAnthropic
ProviderAliyun
ProviderZhipu
ProviderOpenRouter
ProviderSiliconFlow
ProviderDeepSeek
ProviderGemini
ProviderVolcengine
ProviderHunyuan
ProviderMiniMax
ProviderMoonshot
ProviderModelScope
ProviderJina
ProviderNvidia
ProviderAzureOpenAI

这套设计比较适合企业内部场景,因为企业很少只用一个模型。实际部署时,常见组合可能是:

类型选择
对话模型DeepSeek、Qwen、OpenAI、混元
Embeddingbge-m3、text-embedding、Jina
Rerankbge-reranker、Jina rerank、Qwen rerank
VLM多模态理解模型
ASR语音转文本模型

所以 WeKnora 的模型层不是“调用一个 Chat API”,而是把模型能力拆开管理。

RAG 流程:Chat Pipeline 插件化

WeKnora 的问答链路不是写死在一个函数里,而是拆成了一批 chat_pipeline 插件:

must(container.Invoke(chatpipeline.NewPluginSearch))
must(container.Invoke(chatpipeline.NewPluginRerank))
must(container.Invoke(chatpipeline.NewPluginWebFetch))
must(container.Invoke(chatpipeline.NewPluginMerge))
must(container.Invoke(chatpipeline.NewPluginDataAnalysis))
must(container.Invoke(chatpipeline.NewPluginIntoChatMessage))
must(container.Invoke(chatpipeline.NewPluginChatCompletion))
must(container.Invoke(chatpipeline.NewPluginFilterTopK))
must(container.Invoke(chatpipeline.NewPluginQueryUnderstand))
must(container.Invoke(chatpipeline.NewPluginLoadHistory))
must(container.Invoke(chatpipeline.NewPluginExtractEntity))
must(container.Invoke(chatpipeline.NewPluginSearchEntity))
must(container.Invoke(chatpipeline.NewPluginWikiBoost))
must(container.Invoke(chatpipeline.NewMemoryPlugin))

这对二次开发很关键。

如果我要改“PRD 生成助手”,不能只在 prompt 上做文章。真正应该关注的是:

query understand
  -> search
  -> rerank
  -> merge
  -> prompt template
  -> chat completion
  -> post process

PRD 生成不是普通问答,它需要稳定结构、字段约束、历史模板参考、待确认问题归类、HTML 预览输出。只靠用户在聊天框里输入“帮我写 PRD”,效果不会稳定。

Agent / MCP / Skills:能扩展,但不能替代业务流程

源码里已经有 Agent、MCP、Skills、Sandbox、审批机制。

Compose 里有一个 sandbox 镜像:

sandbox:
  image: wechatopenai/weknora-sandbox:${WEKNORA_VERSION:-latest}
  command: ["true"]

注释说明这个容器不是常驻服务,而是 App 执行 Skills 时按需启动,用完释放。

这说明 Skills 更像 Agent 的工具扩展机制,例如:

生成 PRD 初稿
生成 HTML 预览
检查 Markdown 结构
调用外部 API
做数据分析

但它不能直接解决“PRD 生成不是原生强项”的问题。因为 PRD 生成需要的是一条完整工作流:

选择需求类型
  -> 召回历史 PRD
  -> 提取业务规则
  -> 套用模板
  -> 生成正文
  -> 标注待确认问题
  -> 生成 HTML 预览
  -> 支持二次编辑

Skill 可以作为其中的工具节点,但不能替代产品级流程设计。

数据源同步:飞书不是简单读取文本

WeKnora 支持 Feishu、Notion、Yuque、RSS 等数据源同步。源码里有对应 connector 注册:

feishuConnector
notionConnector
rssConnector
yuqueConnector

这也解释了飞书同步为什么会涉及导出权限。它不是只拿页面标题或纯文本,而是需要把飞书文档导出成可解析格式,再交给 docreader 处理。

所以飞书侧权限必须完整,否则就会出现类似:

Access denied.
One of the following scopes is required:
drive:export:readonly
docs:document:export

这里的关键不是“为什么要导出 docx”,而是 WeKnora 的文档入库链路决定了它需要一个统一的解析输入。飞书文档最终要进入 Markdown / chunk / embedding 流程,导出是这个链路的一部分。

部署结构:核心服务和可选服务分得比较清楚

默认核心服务大概是:

frontend   -> Nginx 静态前端
app        -> Go 主后端
docreader  -> Python gRPC 文档解析
postgres   -> ParadeDB / PostgreSQL
redis      -> 异步任务、缓存、PubSub

可选服务包括:

qdrant
milvus
weaviate
doris
neo4j
minio
searxng
langfuse
dex

这个设计比较实际。轻量部署时只启动核心服务,企业部署时再按需打开:

Profile用途
minio本地对象存储
neo4jGraph RAG / 记忆图谱
qdrant / milvus / weaviate替换默认向量库
doris大规模分析型检索
searxngWeb Search
langfuseLLM 可观测
dexOIDC 登录

对本地调研来说,不建议一开始开 full profile。先跑核心链路,再逐个打开可选组件,问题更容易定位。

如果我要基于 WeKnora 做 PRD 生成,应该改哪里

我会优先看这几个目录:

internal/application/service/chat_pipeline/
internal/application/service/
internal/handler/
frontend/src/
skills/preloaded/
config/config.yaml
prompt_templates/

对应改造方向:

目标改造点
固定 PRD 输出结构Prompt 模板、后处理
召回历史 PRDChat Pipeline Search / Rerank
识别业务规则Extract / Entity / Graph 相关逻辑
生成待确认问题Prompt + 后处理
输出 HTML 预览Skill 或独立 Service
前端交互新增 PRD 生成页面
支持飞书资料DataSource Connector 权限和同步链路
多模型配置Model Provider / Model Settings

我不会一开始就改底层向量库,也不会直接重写 Agent。更合理的路径是:

保留 WeKnora 的知识库和检索底座
  -> 增加 PRD 专用 Prompt 模板
  -> 增加 PRD 生成 Service
  -> 增加前端页面
  -> 必要时增加 Skill 做 HTML 预览
  -> 再优化召回和 rerank

结论

WeKnora 的技术栈可以概括为:Go 主后端、Vue 管理前端、Python 文档解析、PostgreSQL/ParadeDB/pgvector 默认检索、多向量库适配、多模型 Provider、Agent/MCP/Skills 扩展。

它适合作为企业知识库和 RAG 平台底座,但不等于开箱即用的 PRD 生成系统。要做 PRD 生成,关键工作不是“接一个大模型”,而是围绕历史资料召回、模板约束、结构化输出、待确认问题、前端交互和 HTML 预览做一层垂直业务封装。

后续我会继续看它的 chat_pipelineskills/preloaded 和数据源同步链路,重点验证两个问题:PRD 场景下召回是否稳定,生成结果是否能被产品直接复制到飞书文档里使用。

你可能还感兴趣