DeepAgent头像
关注
AI Agent 项目赏析:WeKnora——知识库是怎么长成一个 Agent 平台的?封面图

AI Agent 项目赏析:WeKnora——知识库是怎么长成一个 Agent 平台的?

项目类型:企业级知识管理框架 / Knowledge + Agent + Runtime 平台
赏析目标:看一个"文档 → RAG"的知识库,如何演化成"RAG + Agent + Wiki"三条线共享同一知识体系的平台
核心问题:当知识不再是一次性塞进上下文的文本,而是一份持续维护的资产时,Agent 架构会发生什么变化?
源码版本:Tencent/WeKnora,main 分支,commit 4364e61,版本 0.8.2(2026-09-24)

前面十一个项目赏析,我看到了 Agent 的很多面:

LangGraph          → 怎么编排
OpenHands          → 怎么执行真实动作
browser-use        → 怎么感知真实界面
DeerFlow           → 怎么跑长任务
Hermes             → 怎么自我进化
OpenCode           → 怎么组织产品
Goose              → 怎么变成 MCP 服务
DeepSeek Harness   → 怎么做到一切皆插件
OpenMAIC           → 怎么交付产出物
OpenCodeReview     → 什么时候确定性,什么时候交给 Agent

看下来,我发现它们有一个共同的默认假设:

知识是"临时塞进上下文"的东西。

OpenHands:把文件读进来 → 放进上下文 → 用完就没了
browser-use:把页面序列化 → 放进上下文 → 本轮有效
DeerFlow:检索一下 → 放进上下文
OpenMAIC:用户上传的资料 → 提取成 markdown → 当素材用

这些系统里,"知识"的生命周期 = 一次会话(甚至一轮对话)。

WeKnora 走的是另一条路。它的官方定位第一句是:

WeKnora(维娜拉) 是一款开源的、基于大语言模型(LLM)的知识管理框架,面向企业级文档理解、语义检索与智能推理场景。它把团队分散的文档汇集起来,用于检索、推理,并随资料更新持续维护。

注意最后半句——"随资料更新持续维护"。

而它的核心定位,可以用 README 里最精炼的一句话概括:

查询资料用 RAG,处理多步任务用 Agent,整理知识用 Wiki。三种能力共享同一知识库。

        ┌─────────────────────────────────┐
        │        同一个知识库              │
        └────────────┬────────────────────┘
                     │
       ┌─────────────┼─────────────┐
       ↓             ↓             ↓
     RAG           Agent          Wiki
   查询资料      处理多步任务     整理知识

这张图是整篇文章的钥匙。

因为一旦"三种能力共享同一知识库",就出现了前面十一个项目都不需要面对的问题:

Agent 检索到的内容,是谁维护的?       → Wiki 的页面
Wiki 的页面,内容从哪来?              → 从文档来
文档更新了,Wiki 要跟着改吗?          → 要(有 wiki-fixer agent)
Agent 在沙箱里干活,产出的文件归谁?   → 归知识库(Artifacts)
知识库里的知识过期了怎么办?           → 要靠"持续维护"

所以这一篇要回答的是:

当知识从"一次性的上下文材料"升级为"持续维护的资产",架构上必须长出什么?


一、先看清这是什么项目

维度数据
项目WeKnora(维娜拉)
归属腾讯
版本0.8.2(2026-09-24)
最新提交4364e61,2026-09-25
语言Go(后端)+ TypeScript(前端)+ Python(docreader / mcp-server)
协议MIT
客户端Web / 桌面应用(macOS)/ 小程序 / CLI / Chrome 插件 / IM 渠道
生态入口MCP Server、ClawHub Skill、npm 包
数据源本地文件、Confluence、钉钉文档、语雀(IMA)等
沙箱后端Cube / E2B / Docker(+ 桌面模板)
文档语言中 / 英 / 日 / 韩

它的三条产品线(官方能力描述)

能力官方描述
RAG回答有据可查——混合检索、多模态解析、原文引用
Agent用知识和工具完成任务——多步推理、技能与沙箱、本机浏览器、MCP 工具、长期记忆
Wiki把文档整理成 Wiki——自动组织、知识图谱、版本回滚

这三行描述里的每一个词,都是一个工程问题。 随便挑几个:

"原文引用"        → 引用的出处必须能定位(source location)
"技能与沙箱"      → 技能要在隔离环境里跑
"本机浏览器"      → Agent 要能驱动用户自己的浏览器(含登录态)
"长期记忆"        → 跨会话的记忆
"知识图谱"        → 实体与关系抽取
"版本回滚"        → 知识本身要有版本

一个规模参照

internal/ 下的模块(部分):
  agent           → Agent 引擎
  sandbox         → 沙箱(多后端)
  browserskill    → 本机浏览器集成
  mcp / mcpserver → MCP 双向能力
  runtime         → 运行时
  tracing         → 追踪
  filesystem/fs   → 文件系统
  datasource      → 数据源同步
  docreader/      → 文档解析(独立服务)
  models          → 模型集成

这是一个完整的平台级项目——不是"一个 Agent 框架",而是"一套知识 + Agent 的产品体系"。


二、第一层的演化:知识变成"可维护的资产"

先看最基础的一层。

前面十一个项目里,知识是什么?

读一个文件 → 内容进上下文 → 本轮用完 → 消失

知识的生命周期 = 一次上下文窗口。

这样做有一个隐含代价:每次都要重新获取。

第 1 天:用户上传了一份 PDF,问答了一轮
第 2 天:再问同样的问题
  → 又要重新解析、重新检索
  → 而且第 1 天得到的"结论"完全没留下

更根本的问题是:知识无法积累。

文档 A 里的信息 + 文档 B 里的信息
  → 需要"合并理解"才能回答的问题
  → 每次都要从零开始合并

WeKnora 的第一层:把知识沉淀成资产

从 CHANGELOG 的 v0.8.2 里能看到这一层的完整形态:

Knowledge base workflow —— 批量下载原始文件为 ZIP;文档与资源列表可配置排序;停止推进的文档被标记为 queued 或 stalled,并带 TASK_STALLED 码;每次上传可选跳过摘要生成;知识库描述从文档画像生成;上传面板有整体进度、取消与重试;FAQ 列表可按启用状态过滤;活动记录显示 API key 名称。

这一串看起来是"功能列表",但把它们放在一起看,是在处理一个资产系统的必要能力:

批量下载         → 资产可导出(不能锁死)
排序 / 过滤      → 资产可管理
进度 / 取消 / 重试 → 资产导入是长任务,必须可中断
stalled 标记     → 卡住的任务要能被看见(而不是静默挂着)
活动记录 + key 名 → 谁做的操作要能追溯

"知识库"从"一个向量库"变成了"一个有生命周期、有状态、有审计的资产系统"。

而资产的本质特征是:它可以被别的资产引用

这一点在哪体现?在"引用"这件事上。

RAG 能力描述里的 "原文引用",加上 v0.8.2 的这条改进:

Wiki —— 被引用的 chunk 包含图片说明(image captions)

说明"引用"是一个跨子系统的概念:

RAG 回答  → 引用 chunk
Wiki 页面 → 引用 chunk 和图片

一旦有引用,就有"引用完整性"问题:

文档被删了 → 引用它的 Wiki 页面怎么办?
文档更新了 → 引用它的回答还算数吗?

这是"知识成为资产"的必然结果——资产之间会建立关系,关系需要维护。

这一层和前十一篇的对比

知识的生命周期知识的地位
OpenHands / browser-use / DeerFlow一次上下文临时材料
Hermes跨会话(记忆)用户偏好与事实
WeKnora持续维护(资产)组织的知识

Hermes 的记忆是"关于我的",WeKnora 的知识是"关于世界的"。 两者都是"状态离开上下文",但性质完全不同——前者服务于个性化,后者服务于正确性。


三、第二层的演化:Agent 本身成为"资源"

这是我觉得最值得写的一个演化。

在大多数项目里,Agent 是"代码"

OpenCode:一个 agent 定义 = 一段配置(写在代码里)
DeerFlow:lead agent = 一个包
dsh:Agent 是可替换的实现类

Agent 是"被开发出来的东西",不是"被管理的东西"。

WeKnora 里,Agent 是一张数据库表

internal/types/custom_agent.go:

// CustomAgent represents a configurable AI agent (similar to GPTs)
type CustomAgent struct {
	// Unique identifier of the agent (composite primary key with TenantID)
	// For built-in agents, this is 'builtin-quick-answer' or 'builtin-smart-reasoning'
	// For custom agents, this is a UUID
	ID string `yaml:"id" json:"id" gorm:"type:varchar(36);primaryKey"`
	// Name of the agent
	Name string `yaml:"name" json:"name" gorm:"type:varchar(255);not null"`
	// Description of the agent
	Description string `yaml:"description" json:"description" gorm:"type:text"`
	// Avatar/Icon of the agent (emoji or icon name)
	Avatar string `yaml:"avatar" json:"avatar" gorm:"type:varchar(64)"`
	// Whether this is a built-in agent (normal mode / agent mode)
	IsBuiltin bool `yaml:"is_builtin" json:"is_builtin" gorm:"default:false"`
	// Tenant ID (composite primary key with ID)
	TenantID uint64 `yaml:"tenant_id" json:"tenant_id" gorm:"primaryKey"`
	// Created by user ID
	CreatedBy string `yaml:"created_by" json:"created_by" gorm:"type:varchar(36)"`
	...
}

请注意这几个字段:

ID + Name + Description + Avatar   → 它能被"看见"(有名字和头像)
IsBuiltin                          → 它有"官方"与"自定义"之分
TenantID(复合主键)                → 它有归属(多租户隔离)
CreatedBy                          → 它有作者

而注释里那个括号说明了一切:

CustomAgent represents a configurable AI agent (**similar to GPTs**)

"类似 GPTs" —— 这个词是理解这一层的钥匙。

GPTs 是什么?是"可被创建、分享、发现、使用的资源"。 而 WeKnora 的 Agent 有同样的属性:

internal/types/agent_share_source.go     → 分享来源
internal/types/shared_agent_access.go    → 共享访问

"分享"和"共享访问"这两个文件的存在,说明 Agent 已经是资源了 —— 它是可以被传递给别人的东西。

内置了什么?9 个 Agent

BuiltinQuickAnswerID         = "builtin-quick-answer"            // RAG 快速问答
BuiltinSmartReasoningID      = "builtin-smart-reasoning"         // ReAct 多步推理
BuiltinDeepResearcherID      = "builtin-deep-researcher"         // 深度研究
BuiltinDataAnalystID         = "builtin-data-analyst"            // 数据分析
BuiltinKnowledgeGraphExpertID = "builtin-knowledge-graph-expert" // 知识图谱专家
BuiltinDocumentAssistantID   = "builtin-document-assistant"      // 文档助手
BuiltinWikiResearcherID      = "builtin-wiki-researcher"         // Wiki 研究
BuiltinWikiFixerID           = "builtin-wiki-fixer"              // Wiki 修复
BuiltinSkillInstallerID      = "builtin-skill-installer"         // 技能安装器

这份名单本身就在讲一个故事。 因为它不是"一个通用 agent + 几个变体",而是一组职责明确的角色:

问答类:quick-answer
推理类:smart-reasoning
研究类:deep-researcher
数据类:data-analyst
知识图谱:knowledge-graph-expert
运维类:wiki-researcher / wiki-fixer    ← 维护知识本身
平台类:skill-installer                 ← 维护能力本身

最后三个特别值得注意:

wiki-researcher   → 研究知识
wiki-fixer        → 修复知识
skill-installer   → 安装能力

这是"平台自己用 Agent 来维护平台" —— 知识会腐化,所以要有个 agent 专门修它;能力要扩展,所以要有个 agent 专门装它。

这是前十一篇里从未出现过的形态:Agent 不只是"帮用户干活",还在"维护系统自身的资产"。

预设不是"换提示词",而是"换能力组合"

还有一个设计我觉得很聪明——AgentType 预设:

// AgentType constants for Smart-Reasoning agent presets.
// These presets bundle a recommended system prompt template,
// tool allowlist, KB compatibility hint, and other defaults so users
// don't have to configure everything from scratch.
const (
	// AgentTypeRAGQA prefers vector/keyword chunk retrieval on document KBs.
	AgentTypeRAGQA = "rag-qa"
	// AgentTypeWikiQA prefers wiki-page navigation on wiki-enabled KBs.
	AgentTypeWikiQA = "wiki-qa"
	// AgentTypeHybridRAGWiki orchestrates Wiki + RAG on KBs where both are enabled.
	AgentTypeHybridRAGWiki = "hybrid-rag-wiki"
	// AgentTypeDataAnalysis runs SQL / statistics over tabular files (CSV, Excel)
	// uploaded into the KB. Retrieval semantics (vector/wiki/…) are largely
	// irrelevant — this type is about data_schema + data_analysis tools.
	AgentTypeDataAnalysis = "data-analysis"
	// AgentTypeCustom is the "no preset" option; user-configured end to end.
	AgentTypeCustom = "custom"
)

注释说得很清楚:预设捆绑的是 "提示词模板 + 工具白名单 + 知识库兼容性提示 + 其他默认值"。

所以"预设"不是"换个说话风格",而是换一整套能力组合。

而 data-analysis 那条注释尤其精准:

检索语义(向量/wiki/…)基本无关——这个类型是关于 data_schema + data_analysis 工具的。

"对这类 Agent,检索语义基本无关" —— 这句话说明作者真的想过"不同类型的任务,需要的能力是不一样的":

rag-qa     → 需要:向量检索
wiki-qa    → 需要:wiki 页面导航
data-analysis → 需要:SQL 工具(检索?不需要)

而 custom 的定义是"不预填任何东西" —— 这是一个很克制的默认:预设只在用户没主意的时候帮忙,用户要控制权就完全交给他。

这一节的结论

把一个东西从"代码"变成"资源",需要给它:身份(ID/名字/头像)、归属(租户/作者)、生命周期(内置/自定义/删除)、关系(分享/访问控制)。

而这四个属性一旦具备,它就能被管理、发现、分享、审计——从"工程师的产物"变成"用户的东西"。


四、Agent 内核:无状态引擎 + 三段式循环

现在进入 Agent 的运行时。

internal/agent/ 的目录结构本身就说明了很多:

engine.go       → 引擎
think.go        → 思考
act.go          → 动作
steer.go        → 中途引导
finalize.go     → 收尾
checkpoint.go   → 检查点
compaction/     → 上下文压缩
token/          → token 估算
tools/          → 工具
skills/         → 技能
approval/       → 审批
observe.go      → 观测

think.go + act.go + engine.go —— 这是 ReAct 循环最直白的拆法:

engine → 循环驱动
think  → 想
act    → 做

最重要的设计原则:引擎跨轮次无状态

internal/agent/engine.go 的头部注释:

// AgentEngine is the core engine for running ReAct agents.
//
// History persistence note: the engine is stateless across turns. Conversation
// history is rebuilt from the DB once per turn by the caller
// (see service.LoadAgentHistory) and passed into Execute as llmContext. The
// engine therefore does not maintain its own cache, system-prompt store, or
// cross-turn buffer.

翻译:

引擎跨轮次是无状态的。 对话历史由调用方每轮从数据库重建一次(见 service.LoadAgentHistory),并作为 llmContext 传入 Execute。因此引擎不维护自己的缓存、系统提示词存储或跨轮次缓冲区。

这是我在这个系列里第三次看到同一条原则:

DeepSeek Harness:状态在 append-only 日志里,Agent 从中派生
OpenMAIC:执行不在连接里,状态在数据库里
WeKnora:历史不在引擎里,每轮从数据库重建

同一条原则的三种实现,指向同一个结论:

把状态从"执行主体"里搬出去,主体才能被重启、替换、复制、水平扩展。

而"每轮重建"这个做法,看似低效(每次都要读库),但它换来了:

无状态 → 可以水平扩展(任何实例都能处理任何一轮)
无状态 → 可以重启(进程死了,历史还在库里)
无状态 → 容易调试(每轮都是全新构造,没有"上次留下的脏状态")

对于"每轮可能要跑几十秒到几分钟"的 Agent 来说,读一次库的开销完全可以忽略。

上下文管理:校准、降级、跨轮持久化

engine.go 里有三个字段很值得看:

tokenEstimator       *agenttoken.Estimator   // Token estimator for context window management, calibrated
rawEstimator         *agenttoken.Estimator   // Same tokenizer at scale 1, for measuring the calibration
compactor            *compaction.Compactor   // Summarizes older history to fit the context window (nil = disabled)

注意 tokenEstimator 和 rawEstimator 是一对:

rawEstimator    → 原始分词器(scale 1)
tokenEstimator  → 校准后的估算器

为什么要两个? 因为估算 token 一定不准:

不同模型的分词器不同
不同语言的 token 密度不同(中文 vs 英文 vs 代码)
工具返回的 JSON 结构 token 密度又不同

所以他们的做法是:保留原始分词器,再用实际 usage 去校准它。v0.8.2 的 CHANGELOG 里也印证了:

Agent context —— 历史按模型的上下文窗口大小来定量,使用校准过的 token 估算;compaction 检查点跨轮次持久化

"按模型上下文窗口定量 + 校准过的估算" —— 这是个务实的选择:

不追求"精确计算 token"(做不到)
而是"估得足够准,并且知道自己的误差"

检查点:一个"尽力而为"的姿态

internal/agent/checkpoint.go 有一段很好的失败影响分析:

// saveContextCheckpoint writes a compaction that ends on a stored turn onto
// that turn. Best-effort: a failed write costs the next turn one more
// summarization, never this turn anything.
func (e *AgentEngine) saveContextCheckpoint(ctx context.Context, cp *compaction.Checkpoint, round int) {

尽力而为:写失败最多让下一轮多做一次摘要,绝不影响本轮的任何东西。

这句话是"失败影响范围分析"的范例。 它回答了一个每个异步操作都该回答的问题:

这个写失败了会怎样?
  → 下一轮多做一次摘要(可接受的代价)
  → 本轮不受影响(关键:不阻塞主流程)

而如果把这个检查点的失败当成致命错误(比如阻塞本轮、或者重试到成功),后果是:

用户等一个"本可以降级通过"的步骤
  → 延迟上升
  → 可用性下降
  → 而这个数据只是"让下一轮更快"的优化项

"优化项失败不应该影响主流程"——这是所有缓存/检查点类写入都该遵守的原则。

降级检查点:失败了也要留下痕迹

同一个文件里还有一个细节:

if cp.Degraded {
	logger.Warnf(ctx, "[Agent][Round-%d] Saved a degraded context checkpoint through turn %s "+
		"(raw archive, %d chars): the summarizer failed; the next compaction refines it",
		round, cp.TurnID, len(cp.Summary))
	return
}

"降级检查点":

正常情况:摘要器成功 → 保存摘要
降级情况:摘要器失败 → 保存原始归档(raw archive)+ 标记 Degraded
          → 下一轮压缩时再精炼它

降级但绝不丢数据,而且明确告诉下一次"这份是粗糙的"。

"失败时不丢信息,而是存一个粗糙版本并标记" —— 这个模式可以用于很多地方:

摘要失败     → 存原文 + 标记 degraded
翻译失败     → 存原文 + 标记未翻译
索引失败     → 存原始内容 + 标记待索引

关键都是:不要在失败时把数据丢掉,而是降低质量并存下来,把精炼推迟到下次。

一个细节:token 预算按模式分配

internal/types/agent.go:56 有一句很短但信息量很大的注释:

// quick-answer 2048, smart-reasoning 4096, smart-reasoning with a sandbox 24576.
quick-answer                → 2048
smart-reasoning             → 4096
smart-reasoning + sandbox   → 24576   ← 6 倍

为什么带沙箱要 6 倍输出预算?

因为它的输出内容变了:

不带沙箱:回答文字
带沙箱:  回答文字 + 代码 + 命令 + 文件内容 + 脚本说明

"输出预算按任务形态分配",而不是一个全局常量。 这是个很实际的工程判断——把所有模式的 max_tokens 设成一样,要么小得不够用(沙箱场景),要么大得浪费(问答场景)。


五、闪光点一:能力查询,而非类型查询

这是整个项目里我认为最值得学的设计原则。

internal/sandbox/capabilities.go 的包注释:

// Package sandbox: session-scoped capability interfaces.
//
// The Sandbox / Manager pair intentionally hides provider identity (Cube,
// E2B, Docker) from the application layer. Higher layers should never
// branch on Manager.GetType() to decide whether a feature is supported —
// that couples them to a specific backend.
//
// Instead, session-scoped features (shell execution, per-session file
// inspection, attachment staging) are advertised via the capability
// interfaces below. A manager may satisfy the underlying methods yet still
// return nil from the accessors on SessionCapabilityProvider when the
// current runtime configuration cannot honour that capability.

翻译:

Sandbox / Manager 这一对故意对应用层隐藏 provider 身份(Cube、E2B、Docker)。更高层永远不应该通过 Manager.GetType() 来判断某个特性是否被支持——那会把它们耦合到特定后端。

相反,会话级特性(shell 执行、会话内文件检查、附件暂存)通过下面的能力接口来声明。一个 manager 可能实现了底层方法,但当运行时配置无法满足该能力时,仍可以从 SessionCapabilityProvider 的访问器返回 nil。

然后是那个接口:

// SessionCapabilityProvider is implemented by managers that MAY offer
// session-scoped capabilities. Accessors return nil when the current
// runtime configuration cannot support that capability. Application code
// should gate feature registration on non-nil accessor returns.
type SessionCapabilityProvider interface {
	SessionShellExecutor() SessionShellExecutor
	SessionFileStore() SessionFileStore
}

这个设计解决了一个非常常见的问题

先看错误做法(也就是绝大多数项目的做法):

if manager.GetType() == "cube" {
    // 支持 shell
}
if manager.GetType() == "e2b" {
    // 支持 shell
}
if manager.GetType() == "docker" {
    // 支持 shell,但可能不支持终端
}

这个写法有四个问题:

① 应用层知道了后端的名字 → 耦合
② 新增一个后端(比如 Modal)→ 要改所有判断点
③ 能力矩阵散落在各处 → 没人知道"哪个后端支持什么"
④ 最致命的:类型相同但配置不同时,行为不一致

第四点最容易被忽略:

同样是 Cube 后端
  → 配置 A(完整权限)→ 支持 shell
  → 配置 B(受限模式)→ 不支持 shell
  → 但 GetType() 都返回 "cube"

类型查不出来"当前配置下能不能做",只有能力查询能。

正确做法:查能力,不查身份

executor := manager.SessionShellExecutor()
if executor != nil {
    // 注册 shell 特性
    registerShellTool(executor)
}

好处:

① 应用层不需要知道任何后端名字
② 新增后端 → 只要实现(或不实现)接口,应用层零改动
③ 能力矩阵由接口定义说清楚
④ 同后端不同配置 → 访问器可以按配置返回 nil

这条原则的通用形式

❌ 我需要知道你是什么,才能判断你能做什么
✅ 我只需要问你能不能做这件事

它适用于所有"多实现 + 特性差异"的场景:

数据库适配器    → 不要问"你是 MySQL 吗",要问 SupportsSavepoint()
存储后端        → 不要问"你是 S3 吗",要问 SupportsMultipartUpload()
模型 provider   → 不要问"你是 OpenAI 吗",要问 SupportsToolCalling()
沙箱            → 不要问"你是 Cube 吗",要问 SessionShellExecutor() == nil?

而在 LLM 应用里,模型能力的差异特别适合这个模式:

不要:if provider == "anthropic" { 用 vision }
而要:if model.Vision != nil { 用 vision }

(顺带一提,v0.8.2 的"模型目录"改动里就有"视觉支持"这个字段——说明他们确实是这么做的。)

一个额外的精妙处

注意注释里这句话:

A manager may satisfy the underlying methods yet still return nil from the accessors

"实现了方法,但访问器仍返回 nil" —— 这明确允许了"能力存在但当前不可用"的状态。

这是一个比"接口有没有实现"更细粒度的表达:

接口实现了吗?     → 编译期事实
当前能用吗?       → 运行期事实(可能是 nil)

把"能"和"现在能不能"分开表达,是这个设计最精妙的地方。


六、闪光点二:Sandbox 是"可恢复的工作空间",不只是隔离环境

前面几个项目里,沙箱的角色是"隔离执行环境":

OpenHands:Runtime(Docker/Remote/Modal/Runloop),为了安全执行
Goose:Builtin 扩展开 use_docker,为了隔离
dsh:可选隔离(Optional Isolation),为了平衡自由度

它们都在回答"怎么安全地执行"。

WeKnora 的沙箱回答的是另一个问题:"怎么让这个工作空间可以被反复回来"。

证据在哪?

看 v0.8.2 的这三条:

Conversation control —— 向运行中的 agent turn 追加需求(排队到步骤结束,或立即注入);从任意早先的问题 fork 一个对话;原地 rewind,把沙箱工作区重置到匹配的检查点;每个会话选择 reasoning effort。

Sandbox terminal & graphical desktop —— 聊天旁的沙箱面板获得一个交互式终端(Cube / E2B),可以重连到正在运行的 shell;以及一个基于浏览器的 XFCE 桌面(用于从桌面模板构建的沙箱)。

Artifacts library —— 侧边栏"Artifacts"页列出跨对话生成的每个文件,带类型过滤、搜索、日期分组和版本历史。

把它们连起来看:

fork 对话          → 从某个点分叉,各自继续
rewind 对话        → 回到某个点,重来
  ↓ 并且
沙箱重置到匹配检查点 → 沙箱状态跟着回滚!
  ↓ 并且
终端可重连到正在跑的 shell → 环境是活的、可回归的
  ↓ 并且
产物有版本历史      → 产出物也是资产

注意最关键的那一条:

rewind in place, resetting the sandbox workspace to the matching checkpoint

"原地 rewind,把沙箱工作区重置到匹配的检查点" —— 这一条把"对话状态"和"环境状态"绑定在一起了。

为什么这件事很难?

先想一个简单实现会怎样:

用户点了 rewind 回到第 3 轮
  → 对话历史回滚到第 3 轮 ✅
  → 但沙箱里第 4、5 轮生成的文件还在 ❌
  → Agent 重新执行第 4 轮时,看到的是"被污染过的"环境
  → 行为不可预期

这就是"状态和时间不一致":

对话状态:时间旅行回到第 3 轮
环境状态:仍是第 5 轮的样子

而它的解法是:每个检查点也记录沙箱快照,rewind 时一起回滚。

从 internal/sandbox/ 的文件名能看到这套机制:

docker_snapshot.go      → 快照
docker_idle_sweeper.go  → 空闲清理
orphan_reaper.go        → 孤儿回收
cube_terminal.go        → 终端
desktop.go              → 桌面

orphan_reaper.go(孤儿回收) 尤其值得注意——它和前面几篇的问题同构:

DeerFlow:worker 死了 → 孤儿 Run 要恢复
OpenMAIC:工具调用中断 → 孤儿 tool_call 要补票
WeKnora:进程崩了 → 孤儿沙箱要回收

"孤儿资源回收"是长生命周期 Agent 系统的通用问题——因为"申请资源"和"释放资源"之间可能隔着崩溃。

这个沙箱还有"终端"和"桌面"

交互式终端(Cube / E2B)→ 可以重连到正在运行的 shell
XFCE 桌面(浏览器内)    → 桌面模板构建的沙箱

而它们的空闲策略是:

stdin 和 skill-install 命令的输出实时流式
空闲终端/桌面会话在 terminal_idle_disconnect_sec(默认 900 秒)后断开

900 秒(15 分钟) ——这个数字说明它在平衡两件事:

太长 → 资源浪费(用户走了,沙箱还开着)
太短 → 体验断裂(用户去接杯水回来,终端断了)

"给人类留出离开鼠标的时间" ——这是一个产品判断,不是技术判断。

从"隔离环境"到"可恢复工作空间"

传统沙箱WeKnora 的沙箱
核心目的安全(别弄坏宿主机)安全 + 可回归
生命周期一次执行会话级,可暂停/重连/回滚
状态用完即弃有快照、有检查点
与人交互无有终端、有桌面,人能进去操作
与对话的关系无关与对话检查点绑定

"沙箱是 Agent 的工作台,而工作台必须能被找回来" ——这是这一节的结论。


七、闪光点三:BrowserSkill —— Agent 用"你的"浏览器

v0.8.2 最亮的功能是这个。原文(我做了精简,保留关键点):

Local Browser (BrowserSkill) —— smart-reasoning 会话中的 agent 可以通过开源的 BrowserSkill 扩展驱动用户自己的 Chrome 或 Edge。从 Toolbox → Browser Connection 配对一次,在 composer 里打开 "Local Browser",新的 local_browser 工具就能在专属任务窗口里打开页面、点击、填表和读取内容。聊天区显示实时的画中画任务预览,带暂停/恢复/结束控制;登录和验证码通过 request_help 交给用户,被中断的任务可以恢复。

这个设计的核心问题:登录态

先看常规浏览器 Agent 的死穴:

Agent 启动一个干净的浏览器(无状态)
  ↓
打开一个需要登录的页面
  ↓
卡在登录页 ← 这就是绝大多数浏览器 Agent 的实际终点

而前面赏析过的 browser-use,走的是"干净浏览器 + 自动填表"的路线——所以它必须处理:

登录 → 需要账号密码(哪来?)
验证码 → 需要识别(识别不了的呢?)
二次验证 → 需要手机(没有)

这是"Agent 自己的浏览器"这个选择的固有成本。

WeKnora 的解法很直接:

用用户自己的 Chrome/Edge。登录态、cookie、扩展,全都在。

Agent 驱动的是用户的浏览器
  → 用户已经登录的站点,Agent 直接就能用
  → 不需要传账号密码
  → 不需要处理验证码

但它没有回避"必须有人"的情况

这是我觉得最成熟的地方——它明确承认有些步骤必须由人完成,并且把这个承认做成了机制:

登录和验证码通过 request_help 交给用户,被中断的任务可以恢复。

看 internal/browserskill/human.go:

const (
	// HumanStepWait matches BrowserSkill's default help window.
	HumanStepWait = 5 * time.Minute
	// HumanStepTimeout leaves time for the native result before cancellation.
	HumanStepTimeout = HumanStepWait + 15*time.Second
)

// IsHumanStep identifies browser operations that can wait for user input.
func IsHumanStep(method string) bool {
	return method == "request_help" || method == "tab_borrow"
}

这段代码有三个设计点值得学:

① "人类步骤"是一个明确的类型

func IsHumanStep(method string) bool {
	return method == "request_help" || method == "tab_borrow"
}

两类操作被标记为"可能需要等人":

request_help  → 请求帮助(比如"请帮我登录")
tab_borrow    → 借用标签页(用户手动操作后交还控制权)

"人机交接"被显式建模成操作类型,而不是散落在各处的特殊情况处理。

② 超时按操作语义分类

普通操作     → 正常超时
人类步骤     → 5 分钟(HumanStepWait)

5 分钟——因为:

太短 → 用户还没输入完验证码,连接就断了
太长 → Agent 在一个可能没人理的请求上挂太久

③ 那个 +15 秒

// HumanStepTimeout leaves time for the native result before cancellation.
HumanStepTimeout = HumanStepWait + 15*time.Second

"给原生结果留出时间,然后再取消。"

这个 15 秒的含义是:

用户在 5 分钟内完成了操作
  ↓
操作结果需要一点时间返回(浏览器要执行、页面要加载)
  ↓
如果超时时间正好是 5 分钟,就可能"用户刚做完,连接就被掐了"
  ↓
+15 秒 = 给结果回来的缓冲

"超时不是一个参数,而是三段:等待时间 + 结果缓冲。" 这个细节很真实——它只在真实使用中才会暴露出来(用户抱怨"我明明做完了他却说超时")。

④ 而且它还能恢复

被中断的任务可以恢复(interrupted tasks can be resumed)

人机交接的常见失败是"用户没及时响应",而它的处理是"可恢复"而不是"重头再来"。

这一节和前面对照

browser-use:Agent 自己的浏览器 → 自动化程度高,但登录态是死穴
WeKnora:  用户的浏览器       → 登录态免费,但需要人机交接机制

这是同一个问题的两种答案,而 WeKnora 的答案承认了"自动化不能覆盖一切"。

把"必须有人"这件事做成机制,比假装它不存在要成熟得多。


八、闪光点四:Skill 的三层契约

internal/agent/skills/ 的设计里有三个我认为很有价值的东西。

第一层:环境变量契约(平台与技能的接口)

manager.go 的头部注释:

// artifactOutputEnvVar is the name of the environment variable that WeKnora
// injects into every skill script execution. The value points to the
// convention-driven directory where the script should drop artifacts the user
// will be able to download after the turn completes.
//
// The name is stable across releases; skills reference it via os.getenv(...)
// so they never hard-code the path.
const artifactOutputEnvVar = "WEKNORA_SKILL_OUTPUT_DIR"

// sessionInputEnvVar points skill scripts at user-uploaded files restored into
// the current session's Cube. Inputs are separate from generated artifacts.
const sessionInputEnvVar = "WEKNORA_SESSION_INPUT_DIR"

// artifactHistoryEnvVar is the name of the environment variable that points
// to the root artifact output directory (/workspace/output). Skill scripts
// can use this to self-discover artifacts from prior runs when they need to
// chain without LLM mediation.
const artifactHistoryEnvVar = "WEKNORA_SKILL_HISTORY_ROOT"

三个环境变量,构成了一个清晰的约定:

WEKNORA_SKILL_OUTPUT_DIR    → 本次产出放哪(用户之后可下载)
WEKNORA_SESSION_INPUT_DIR   → 用户上传的输入在哪(与产出分开)
WEKNORA_SKILL_HISTORY_ROOT  → 历史产出的根目录(/workspace/output)

注意注释里的两个设计决定:

① "名字跨版本稳定"

The name is stable across releases; skills reference it via os.getenv(...) so they never hard-code the path.

(名字跨版本稳定;技能通过 os.getenv(...) 引用,因此永远不硬编码路径。)

这就是"平台与技能之间的接口契约":

平台:我保证这个环境变量名永远存在,值指向正确的目录
技能:我用它,不关心实际路径是什么

因为平台可以改目录结构(沙箱换了、路径变了),但技能不该跟着改。

② "输入和产出分开"

Inputs are separate from generated artifacts.

(输入与生成的产物是分开的。)

这个区分很重要:

不分开 → 用户上传的文件和技能生成的文件混在一起
       → 用户分不清哪些是"我给的",哪些是"它做的"
       → 清理时不敢删(怕删了输入)或者乱删

第二层:技能之间"不经 LLM 中介"地协作

这是第三个环境变量的说明里最精彩的一句:

Skill scripts can use this to self-discover artifacts from prior runs when they need to chain without LLM mediation.

翻译:

技能脚本可以用它来自发现之前运行的产物,从而在需要时不经 LLM 中介地链式协作。

"chain without LLM mediation"(不经 LLM 中介的链式协作) —— 这句话值得单独拎出来。

它说的是这样一种情况:

技能 A 产出了一个文件
  ↓
技能 B 需要这个文件作为输入
  ↓
❌ 常规做法:A 产出 → 告诉模型 → 模型理解 → 模型决定调 B → 把文件信息交给 B
             (消耗 token、可能传错、可能理解偏差)
✅ 它的做法:B 直接从 WEKNORA_SKILL_HISTORY_ROOT 里找到文件

"能用文件系统约定的,不要绕一圈 LLM。"

这个原则的价值可以量化:

绕 LLM:每次传递 = 一次 token 消耗 + 一次失败可能 + 一次理解偏差
用文件:每次传递 = 一次目录读取(零成本、零偏差)

而它背后是一个更普遍的原则:

确定性数据传递,不要经过概率系统。

这和上一篇文章(OpenCodeReview)里那条"什么时候确定性,什么时候交给 Agent"是同一条原则——只不过这里的分界线画在"技能与技能之间"。

第三层:多租户隔离

internal/agent/skills/ 里有两个文件:

source.go          → 来源
tenant_source.go   → 租户来源

skill 是分租户的。 而结合 v0.8.2 里这条:

Skills —— 过期的沙箱技能可以在不回滚目录、不使技能下线的情况下升级;安装后会验证运行时前置条件;带 UTF-8 BOM 的清单也能加载。

"升级技能时不回滚目录、不下线" —— 这是个很实际的可用性要求:

企业里,一个技能可能被很多 Agent 引用
  ↓
升级它
  ↓
如果"先下线再升级"→ 这段时间所有引用的 Agent 都坏了
如果"不回滚目录、不下线"→ 平滑升级

这是"能力的分发"必须处理的工程问题——一旦 skill 是资产(可上传、可安装、可分享),它就有了版本和兼容性问题。


九、闪光点五:MCP 的双向性,与"按需发现工具"

MCP 双向:既是客户端,也是服务端

前面赏析 Goose 的时候,MCP 是"Agent 消费外部工具能力"的方式:

Goose:Agent → MCP client → 外部 MCP server(70+ Extensions)

而 WeKnora 有两个 MCP 目录:

internal/mcp/         → MCP 客户端(消费外部工具)
internal/mcpserver/   → MCP 服务端(把能力暴露出去)

v0.8.2 的 CHANGELOG:

Built-in MCP Server —— 每个 workspace 可以在 /mcp/<endpoint_id> 发布 MCP 端点(Streamable HTTP),从 Settings → Integrations → MCP Server 配置,有自己的 token、知识库范围、每分钟限流(默认 60)和工具组:retrieval & reading、ask(运行该端点的默认 agent)、Wiki,以及写工具(默认关闭)。页面会生成 Cursor / VS Code / Claude Desktop 的 mcpServers 配置和一行 Claude Code 命令;token 可以轮换。端点通过 /api/v1/mcp-endpoints 管理。Python 的 mcp-server/ 目录现已废弃。

"把知识库暴露成一个 MCP 端点" —— 这个方向很有意思:

常规:Agent 找工具(Agent 是消费者)
它这里:知识库变成工具,被别人消费(知识库是提供者)

而配置里的每个细节都是产品判断:

自己的 token           → 可撤销、可轮换
知识库范围             → 只能访问被授权的知识(最小权限)
每分钟限流(默认 60)   → 防滥用
工具组                 → retrieval / ask / wiki / write
写工具默认关闭         → 默认安全
生成 Cursor/VS Code/Claude 配置 → 降低接入成本
token 可轮换           → 泄露后可补救

"写工具默认关闭" 这一条尤其重要:默认给的是只读能力,要写就得显式开。 这是"默认安全"的正确姿势。

MCP 客户端:按需发现,而不是全量加载

MCP client —— 工具目录按服务、按调用者持久化,带引导式两步设置和 AI 起草的使用说明;工具可以单独启用或禁用;agent 通过 discover_mcp_tools / call_mcp_tool 按需发现和调用 MCP 工具,而不是加载每一个工具;advanced_config.timeout 可以延长调用窗口;多行 SSE 事件被正确组装。

"按需发现和调用,而不是加载每一个工具" —— 这是"渐进式披露"在 MCP 上的应用:

❌ 把所有 MCP 工具都塞进系统提示词
   → 假设 10 个 MCP 服务,每个 20 个工具 = 200 个工具定义
   → 光是工具定义就吃掉大量上下文

✅ agent 通过 discover_mcp_tools 按需查找
   → 系统提示词里只有两个元工具
   → 需要时才把具体工具拉进来

这和 OpenHands 的 Skills 渐进披露、Hermes 的 skill 加载是同一思路,只不过应用对象从"技能"换成了"MCP 工具"。

模式是一样的:常驻上下文放"目录 + 访问方式",具体内容按需拉取。


十、闪光点六:能力演进时,怎么不破坏存量数据

这是我在这个项目里学到的另一个很实用的工程手法。

v0.8.2 做了一次工具大合并:

knowledge_search + grep_chunks
  → 合并为 search_knowledge(带 mode: hybrid | semantic | keyword)

list_knowledge_chunks + get_document_info + wiki_read_source_doc
  → 合并为 read_document

新增 list_documents

工具合并是好事(工具越少,模型选择越准),但它带来一个严重问题:

存量数据里,Agent 配置写的是旧工具名
预设里、API 调用里,都是旧工具名
已经存储的对话历史里,也引用了旧工具名
  ↓
改名 → 所有存量配置全部失效

这是"能力演进"最现实的成本。

而它的解法是运行时别名映射:

Backward-compatible tool aliasing —— 存储在 agent 配置、预设和 API 调用中的旧工具名会在运行时映射到它们的后继者(legacyToolSuccessors / NormalizeAllowedTools);不需要数据迁移,而且引用了旧工具名的已存聊天历史仍能正常渲染。

三个关键点:

① 运行时映射(legacyToolSuccessors)   → 不改存量数据
② 不需要数据迁移                        → 没有迁移窗口、没有迁移失败风险
③ 已存历史仍能渲染                      → 用户看不到"历史坏了"

为什么选运行时映射而不是数据迁移?

数据迁移:
  ✗ 需要停机窗口(或双写期)
  ✗ 迁移可能失败,要回滚方案
  ✗ 存量数据有多少、分布如何,要盘清楚
  ✗ 如果有外部系统引用了旧名字,迁不完

运行时映射:
  ✓ 零停机
  ✓ 可回滚(删掉映射表就回到旧行为)
  ✓ 新旧名字可以共存(渐进淘汰)
  ✗ 代价:多一层间接(每次调用都要查映射)

对一个已被大量用户使用的平台来说,"多一层间接"远比"一次数据迁移"便宜。

这条经验的通用形式:

当你要改一个"已经被存储/被引用"的标识符时,优先考虑运行时映射,而不是数据迁移。

而它还有一个隐含要求——映射表本身要能被删除:

老的映射保留多久?
  → 等历史对话自然过期
  → 或者永远保留(反正只是一张很小的表)

十一、闪光点七:降级要诚实,不要假装成功

这个项目里有一处改进,我认为体现了很成熟的工程态度。

Retrieval tool compatibility fixes —— search_knowledge 不再拒绝某个知识库无法提供的 mode:FAQ 和纯向量库会语义地回答 keyword 请求,纯关键词库会用关键词回答 semantic 请求,结果里带 requested_mode / mode_fallbacks,模型渲染里有 <mode_fallback>,渲染现在显示实际使用的 mode,而不是永远显示 semantic。

这条在解决什么?

先看背景:search_knowledge 有三个 mode:

hybrid    → 混合检索
semantic  → 语义(向量)
keyword   → 关键词(BM25)

问题是:不是每个知识库都支持所有 mode。

FAQ 库       → 没有向量索引
纯向量库     → 没有关键词索引
纯关键词库   → 没有向量索引

旧的实现是:直接拒绝。

用户/模型请求 mode=keyword
  → 这个库不支持
  → ❌ 报错:"不支持该 mode"

这是个很差的体验:

模型不知道这个库支持什么
  → 它按 schema 说的选了 keyword
  → 被拒绝
  → 它可能重试、可能放弃、可能换个库
  → 而用户看到的是"检索失败"

新实现:降级,但明说。

请求 mode=keyword(库里没有关键词索引)
  → ✅ 用语义检索回答
  → 结果里带:requested_mode: "keyword", mode_fallbacks: [...]
  → 模型渲染里带 <mode_fallback>
  → 显示的是"实际使用的 mode",而不是永远显示 semantic

三个设计点

① 降级而不是失败

能力不满足 → 用最接近的能力兜住(而不是报错)

因为"用语义检索回答关键词查询"虽然不理想,但比"什么都不返回"好得多。

② 把"降级"显式告诉调用方

requested_mode  → 你请求的是什么(保留原始意图)
mode_fallbacks  → 实际发生了什么降级

"我按你说的做了,但方式和你期望的不同" —— 这个信息很有价值:

对模型:它知道这次检索不是它想要的方式 → 可以调整策略
对用户:可追溯(为什么这次结果不太一样)
对调试:能看出"降级"是不是常态 → 决定要不要补索引

③ 最后那半句最诚实

渲染现在显示实际使用的 mode,而不是永远显示 semantic

"而不是永远显示 semantic" —— 说明旧的实现会显示一个错误的 mode。

旧:实际用了 keyword,界面显示 semantic   ← 撒谎
新:实际用了什么就显示什么,并标注降级     ← 诚实

"UI 上显示的内容必须反映实际发生了什么" —— 这是可观测性最基本也最容易被违背的原则。

因为一旦界面撒谎:

用户基于错误信息做决策
  → 排查问题时被误导
  → 积累成"这个系统不可信"

十二、代价:平台级复杂度的四个来源

前面讲的都是收益。现在讲代价,而且这个项目的代价很典型。

代价一:安全面极大

v0.8.2 里有一条很值得注意的安全改进:

Whitelist-only outbound mode —— SSRF_DNS_WHITELIST_ONLY=true 在 DNS 解析之前就拒绝 SSRF_WHITELIST 之外的任何主机,关闭离线部署中基于 DNS 的外泄。

"在 DNS 解析之前拒绝" —— 这个时机选择很关键:

错误做法:解析 DNS → 得到 IP → 判断 IP 是否在内网 → 拒绝
  问题:DNS 查询本身已经发出去了!
       → 攻击者可以通过"查询哪个域名"来编码并外泄数据(DNS exfiltration)

正确做法:在解析之前,先看域名白名单 → 不在白名单就不解析
  效果:连 DNS 查询都不会发出

"拒绝的时机要足够早" —— 早在副作用发生之前。

而这也说明它面对的攻击面:

用户上传文档          → XXE、恶意文档
文档里有 HTML/JS      → 渲染时的注入
Agent 访问 URL        → SSRF、DNS rebinding
Agent 生成 HTML 产物  → 在沙箱里渲染
本机浏览器            → 借用户身份访问站点
桌面沙箱(XFCE)      → 完整图形环境
对象存储              → 凭据、重定向、SSRF

对比一下前面几篇:

项目主要安全面
OpenCodeReview读代码(本地,低风险)
OpenCode / Goose改代码(本地)
OpenMAIC渲染 HTML + 生成视频(服务端)
WeKnora文档解析 + URL 访问 + 沙箱 + 用户浏览器 + 桌面 + 对象存储

它的攻击面是前面所有项目里最宽的——因为它同时有"文档理解"、"网络访问"、"代码执行"、"浏览器控制"、"图形桌面"五种能力。

而每一种能力被 Agent 自动调用时,都是一条潜在的注入路径。

代价二:兼容成本极高

数一下它要支持的维度:

数据源:本地文件 / Confluence / 钉钉文档 / 语雀 / IMA / …
沙箱后端:Cube / E2B / Docker / 桌面模板 / 本机(Seatbelt)
模型厂商:27 个内置厂商
客户端:Web / 桌面(macOS) / 小程序 / CLI / Chrome 插件 / IM
协议:MCP client / MCP server / HTTP API / SSE
语言:中 / 英 / 日 / 韩

27 个模型厂商这个数字最能说明问题。而这背后是一整套三层抽象(v0.8.2 的"模型目录"改动):

Model catalog —— 模型集成在协议、厂商、目录三层上重建:27 个内置厂商,一个生成的目录填充上下文窗口、最大输出、推理级别、视觉支持,模型编辑器里有 "resolved call" 面板,支持按模型的协议覆盖和协议兼容 JSON,GET/POST /models/catalog/resolve,以及一个部署覆盖文件(config/models.json、MODELS_CONFIG)。

拆开看这三层:

协议层    → 同一套协议兼容多个厂商(OpenAI 兼容 / Anthropic / …)
厂商层    → 厂商级配置(endpoint、密钥、默认值)
目录层    → 每个模型的能力元数据(上下文窗口、最大输出、推理级别、视觉)

为什么要分三层? 因为变化的维度不同:

新出一个模型      → 只动目录层
厂商改了 API 版本 → 动厂商层
出现新协议        → 动协议层

"按变化的维度分层" —— 这是抽象层该怎么切的标准答案。

而 resolved call 面板这个细节也很有意思:

"显示最终解析出的调用是什么"
  → 因为配置有 3 层叠加,用户很难在脑子里算出最终值
  → 界面直接告诉他结果

这和我之前写 DeepSeek Harness 那篇里的 --dump-config 是同一个思路:配置分层之后,必须提供一个"看最终生效值"的手段。

代价三:一致性成本

当"三种能力共享同一知识库"时,就出现了一致性问题。

举几个从 CHANGELOG 里能看到的具体冲突:

"同标题条目跨类型复用现有页面"        → Wiki 的命名冲突
"整页重写被完成预算截断时继续,若仍不完整则拒绝" → 写入的原子性
"有歧义的单汉字自动链接被跳过"        → 自动链接的准确率 vs 覆盖率
"引用 chunk 包含图片说明"             → 跨层引用完整性
"同标题条目跨类型复用"                → 去重 vs 区分

而这条最能说明问题:

Wiki —— 整页重写在被完成预算截断时继续,如果仍然不完整则拒绝。

写一个 Wiki 页面
  → 输出被 max_tokens 截断了
  → 继续写(而不是留下半截页面)
  → 如果续写后还是不完整 → 拒绝写入

"宁可拒绝,也不留半截" —— 因为 Wiki 页面是要给人读的资产,半截页面的危害大于"这一页没写成"。

(这和 DeepSeek Harness 那条"宁可不留,也不留半截"是同一种洁癖——只不过那里是"日志",这里是"知识资产"。)

代价四:长任务带来的"卡住"问题

停止推进的文档被标记为 queued 或 stalled,并带 TASK_STALLED 码

"stalled"(停滞)这个状态的存在,说明"卡住"是常态而不是异常。

文档导入是个长任务(解析、切块、向量化、抽取实体、生成摘要……)
  ↓
任何一步都可能卡住(外部 API 慢、文件太大、解析器挂了)
  ↓
如果没有 stalled 状态:
    → 任务永远显示"处理中"
    → 用户不知道是在跑还是死了
    → 只能干等

"给'卡住'一个明确的状态" —— 这是长任务系统的必需能力。

而它还需要配套:

可取消      → 上传面板有取消
可重试      → 上传面板有重试
可诊断      → stalled 有明确的 code
可见        → 整体进度

"长任务四件套:状态、取消、重试、诊断。" 缺任何一个,用户就会陷入"不知道该怎么办"的状态。


十三、和前面十一个项目放在一起看

知识在哪Agent 是什么沙箱是什么平台性体现在哪
LangGraph上下文图的节点——
OpenHands上下文Runtime 的使用者隔离执行—
browser-use页面感知循环——
DeerFlow上下文Run 的驱动者provider—
Hermes记忆长驻主体—多实例
OpenCode上下文产品内核—多客户端
Goose上下文MCP 客户端可选 Docker扩展生态
dsh日志可替换插件可选隔离组装/Profile
OpenMAIC素材内容生产者—产出物工具链
OpenCodeReview上下文确定性+Agent—多入口
WeKnora知识库(可维护资产)可管理的资源可恢复工作空间三种能力共享同一知识体系 ← 本篇

看最后一行和上面所有行的差别:

前面所有项目:Agent 是主体,知识是它的燃料
WeKnora:知识是主体,Agent 是它的使用方式之一

这个关系反转,是这篇文章最想表达的东西。

而一旦反转,架构上就必须长出:

知识要有生命周期     → 状态、stalled、版本、回滚
知识要能被引用       → 引用完整性
知识要能被维护       → wiki-fixer agent(Agent 维护知识)
知识要能被共享       → MCP server 把知识暴露出去
知识要能被追溯       → 活动记录、API key 名、引用出处
Agent 要能被管理     → Agent 成为资源(ID/头像/归属/分享)
执行要能被回归       → 沙箱检查点与对话 rewind 绑定
能力要能被扩展       → Skill(有契约、有版本、有租户隔离)

这九件事,是"知识成为主体"的必然要求。


十四、如果你想读它,建议的路线

第 0 步  README_CN.md(15 分钟)
         ★ 那张"查询资料用 RAG,处理多步任务用 Agent,整理知识用 Wiki"
         的三线图是理解全项目的入口

第 1 步  CHANGELOG.md 的 [0.8.2] 段(40 分钟)
         ★★ 全项目性价比最高的一段。它按能力分组列出了 v0.8.2 的所有改动,
         相当于一份"这个平台现在长什么样"的完整清单

第 2 步  internal/types/custom_agent.go(25 分钟)
         ★ Agent 作为资源:9 个内置 Agent + 预设类型 + 多租户字段

第 3 步  internal/agent/engine.go 的头部注释(15 分钟)
         ★ "引擎跨轮次无状态" + token 校准的一对估算器 + compactor

第 4 步  internal/sandbox/capabilities.go 的包注释(25 分钟)
         ★ "能力查询而非类型查询"。这是我认为最值得单独学的一条

第 5 步  internal/agent/checkpoint.go(20 分钟)
         ★ 尽力而为的检查点 + 降级检查点

第 6 步  internal/agent/skills/manager.go 的头部(20 分钟)
         ★ 三个环境变量契约 + "不经 LLM 中介的链式协作"

第 7 步  internal/browserskill/human.go(15 分钟)
         ★ 人机交接 + 超时三段(5 分钟 + 15 秒)

第 8 步  按需:internal/agent/{think,act,steer}.go、internal/mcpserver/、docreader/

不要从 frontend/ 开始
不要从 migrations/ 开始(表结构细节)
不要跳过 CHANGELOG —— 它是这个平台最好的架构地图

十五、我认为最值得带走的五个工程原则

第一:问"你能不能做",不要问"你是什么"

❌ if manager.GetType() == "cube" { 支持 shell }
✅ executor := manager.SessionShellExecutor(); if executor != nil { 用 }

能力查询(capability query)永远优于类型查询(type query)——因为"同一种类型在不同配置下,能力可能不同"。

第二:优化项失败,不应该影响主流程

检查点写失败 → 下一轮多做一次摘要(可接受)
             → 本轮不受影响(关键)

给每一个异步写入回答一个问题:"它失败了会怎样?"如果答案是"拖慢一点",那就绝不该让它阻塞主流程。

第三:确定性的数据传递,不要经过概率系统

❌ 技能 A 产出 → 告诉模型 → 模型理解 → 模型转交给技能 B
✅ 技能 B 直接从约定目录找到 A 的产出

能用文件系统(或任何确定性通道)约定的,不要绕一圈 LLM。 那不是"更智能",那是"更贵、更慢、更容易错"。

第四:降级要显式,界面不能撒谎

请求 keyword,实际用了 semantic
  → 结果里带 requested_mode 和 mode_fallbacks
  → 界面显示"实际使用的方式"

"我按你说的做了,但方式不同"——这个信息必须传下去。 而界面上显示的内容,必须反映实际发生了什么。

第五:改一个被引用的标识符时,先想想能不能不迁移

旧工具名 → 运行时映射到新工具名
  → 不需要数据迁移
  → 存量历史仍可渲染

数据迁移有停机窗口和失败风险;运行时映射只有一层间接。 对已经被大量引用的标识符,后者通常是更便宜的答案。


十六、工程评价

维度评价说明
产品完整度★★★★★文档 → 解析 → 检索 → Agent → 沙箱 → Wiki → 共享,闭环完整
架构原则性★★★★★能力查询、无状态引擎、降级诚实——多处体现了清晰的原则
多后端抽象★★★★★沙箱(Cube/E2B/Docker)、模型(27 厂)、数据源,抽象层次切得准
长任务工程★★★★★状态机、检查点、stalled 标记、取消/重试/诊断齐全
安全投入★★★★☆SSRF 多层防护、默认只读、Seatbelt 沙箱;但攻击面客观最宽
状态设计★★★★★引擎无状态 + 沙箱有检查点 + 知识有版本,三种状态各得其所
兼容性工程★★★★★工具别名映射(零迁移)、mode fallback、多语言多协议
文档与更新★★★★★CHANGELOG 写得极详尽(可直接当架构文档读)
部署复杂度★★★☆☆依赖多(向量库、图库、沙箱、对象存储),完整跑起来门槛不低
上手门槛★★★☆☆有 Lite 模式和 Docker Compose,但平台概念多
小项目适用性★★☆☆☆它是平台,不是框架;但它内部单个模块的模式非常可借鉴
工程参考价值★★★★★"知识作为资产"这一块的最佳参考,且内部有大量可复用的小模式

它最大的优点:

它把"知识"从"Agent 的燃料"提升成了"平台的主体"——而且不是靠一句定位,是靠一整套机制(生命周期、引用完整性、版本、维护 Agent、对外共享、审计)。

它最大的缺点:

平台级复杂度,且攻击面最宽。 文档解析、网络访问、代码执行、浏览器控制、图形桌面——五种能力叠加,每一条都是潜在的注入路径。它的安全投入也很可观,但这说明它必须投入。

最值得你立刻拿走的三点:

① 能力查询 > 类型查询
   → 不要问"你是什么",要问"你能不能"

② 优化项失败不能阻塞主流程
   → 给每个异步写入回答"它失败了会怎样"

③ 确定性传递不要经过 LLM
   → 能用文件/约定传的,不要绕模型

十七、不要抄 WeKnora,要学 WeKnora

最后一节照旧,而且这次尤其要说清楚。

它不是一个"该不该抄"的项目——因为它根本抄不动。

依赖:向量库 + 图库 + 沙箱集群 + 对象存储 + 27 家模型 + 多语言
模块:agent / sandbox / browserskill / mcp / mcpserver / docreader / datasource / ...

如果照搬它的架构,你会得到一个有 30 个模块、在你只有 3 个用户的场景下大部分都在空转的系统。

但它内部有一批"可以单独拿走"的模式,而且这些模式和它的规模无关:

可拿走的模式什么时候用得上
能力查询接口(返回 nil 表示不可用)任何时候你有"多种后端 + 能力差异"
无状态引擎 + 每轮重建历史你的 Agent 需要水平扩展或能重启
尽力而为的检查点你有任何"优化项"类写入
降级检查点(存原文 + 标记)你有任何"摘要/翻译/索引"类步骤
运行时映射代替数据迁移你要改一个已被引用的标识符
环境变量契约(跨版本稳定)你有"平台 + 插件"的关系
"实际使用的方式"要显示出来你有任何 fallback / 降级逻辑
人机交接的类型化 + 超时分段你的自动化里必须有人工步骤
stalled 状态 + 取消/重试/诊断你有任何长任务

这九个模式,每一个都可以独立用在你的项目里,而且都不需要"先有一个平台"。

而 WeKnora 最值得学的地方,其实是一个判断:

它想清楚了"知识"在这个系统里是什么地位——不是燃料,是资产。

然后所有架构都从这个判断推出来。

先想清楚你的系统里"什么才是主体",架构会自己长出来。


十八、结语

回到标题那个问题。

知识库是怎么长成一个 Agent 平台的?

从源码里能看到清晰的三个阶段:

第一阶段:文档 → 解析 → 切块 → 向量化 → 检索
          (知识库作为 RAG 的数据层)

第二阶段:+ Agent(ReAct 循环、工具、沙箱、技能)
          知识开始被"使用",而不只是被"查询"

第三阶段:+ Wiki(自动组织、知识图谱、版本回滚、维护 Agent)
          知识开始被"维护",走出了一条反哺的路

而第三阶段是关键——因为它是唯一一个"知识反过来变成生产物"的阶段:

第一、二阶段:文档 → 知识(单向)
第三阶段:    知识 → Wiki → 更好的知识(循环)

有了这个循环,"知识库"才真正变成了"平台"。

而这个演化最终给出的答案是:

当知识成为资产,Agent 才有了可以积累的东西。

前十一篇赏析里,Agent 学到的每一件事,都随会话结束而消失(除了 Hermes 的个性化记忆)。而这里,Agent 的产出会沉淀成知识,知识又成为下一次任务的起点。

最后,我想用这个项目里最打动我的一段代码收尾——不是某个精巧的算法,而是一句注释里对"失败"的态度:

// ... Best-effort: a failed write costs the
// next turn one more summarization, never this turn anything.

"写失败最多让下一轮多做一次摘要,绝不影响本轮。"

这句话里有三个值得学的东西:

① 它明确分析了失败的影响范围(而不是笼统地"处理错误")
② 它接受了"优化项可以失败"(而不是追求 100% 成功)
③ 它划清了"绝不能影响"的边界(本轮主流程)

一个成熟的系统,不是"什么都不出错"的系统,而是"想清楚了每一处失败会造成什么后果"的系统。


附录 · 本文引用到的真实位置

内容位置
三线定位 / 产品介绍README_CN.md
v0.8.2 全部改动(BrowserSkill / MCP Server / 工具合并 / 别名 / 上下文 / SSRF)CHANGELOG.md 的 [0.8.2] - 2026-09-24
版本号VERSION(0.8.2)
Agent 作为资源 / 9 个内置 Agent / 预设类型internal/types/custom_agent.go
token 预算按模式分配internal/types/agent.go:56
无状态引擎 / token 校准 / compactorinternal/agent/engine.go(头部注释)
尽力而为检查点 / 降级检查点internal/agent/checkpoint.go
能力查询而非类型查询internal/sandbox/capabilities.go(包注释 + SessionCapabilityProvider)
沙箱多后端与终端/桌面internal/sandbox/{cube_*, docker_*, e2b_*, desktop, orphan_reaper, docker_idle_sweeper}.go
人机交接与超时分段internal/browserskill/human.go
Skill 环境变量契约internal/agent/skills/manager.go(头部常量注释)
Skill 租户隔离internal/agent/skills/{source, tenant_source}.go
ReAct 三段式internal/agent/{engine, think, act, steer, finalize}.go
MCP 双向internal/mcp/、internal/mcpserver/
数据源同步internal/datasource/
文档解析docreader/

关于 star 数:文中未引用具体 star 数量。该项目在 README 中挂有 Trendshift 徽章,可确认其处于增长榜单中;如需精确数字,建议以 GitHub 仓库页为准。

系列导读:本文是「AI Agent 项目赏析」系列第 12 篇。

1.  LangGraph          → Workflow Agent(任务怎么编排)
2.  LangGraph(进阶)   → Agent Orchestration
3.  OpenHands          → Coding Agent(怎么执行真实动作)
4.  browser-use        → 浏览器 Agent(怎么感知真实界面)
5.  DeerFlow 2.0       → SuperAgent(长任务怎么跑不死)
6.  Hermes Agent       → Persistent Agent(怎么自我进化)
7.  OpenCode           → Terminal Agent(产品怎么组织)
8.  Goose              → MCP-native Agent(工具怎么变成服务)
9.  DeepSeek Harness   → Plugin-native Runtime(连 Loop 都是插件)
10. OpenMAIC           → Product-oriented System(产出物才是主角)
11. OpenCodeReview     → Hybrid Agent(什么时候确定性,什么时候交给 Agent)
12. WeKnora            → Knowledge Agent Platform(知识才是主体)← 本篇

十二篇之后,这条赏析路线覆盖的形态:

怎么编排 → 怎么执行 → 怎么感知 → 怎么长跑 → 怎么进化
→ 怎么组织产品 → 怎么扩展 → 怎么组合 → 怎么交付产出物
→ 怎么划分确定性与模型 → **怎么让知识成为可以积累的资产**

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_85223846/article/details/166728387

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--