最近我在调研腾讯开源的 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)
这里有两个值得注意的点:
embedding::halfvec(dim)不是多余的转换,而是为了匹配 HNSW 表达式索引。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 vector | Lite 模式、本地单机体验 |
| 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、混元 |
| Embedding | bge-m3、text-embedding、Jina |
| Rerank | bge-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 | 本地对象存储 |
| neo4j | Graph RAG / 记忆图谱 |
| qdrant / milvus / weaviate | 替换默认向量库 |
| doris | 大规模分析型检索 |
| searxng | Web Search |
| langfuse | LLM 可观测 |
| dex | OIDC 登录 |
对本地调研来说,不建议一开始开 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 模板、后处理 |
| 召回历史 PRD | Chat 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_pipeline、skills/preloaded 和数据源同步链路,重点验证两个问题:PRD 场景下召回是否稳定,生成结果是否能被产品直接复制到飞书文档里使用。
评论 / 0
共 0 条你可能是第一个留下评论的人