数字员工是什么?
在写字楼里多了一位"智能新同事"——TA 有工号、有人设、有技能、有飞书号。
先想象一个场景
周一早上九点,你在飞书群里 @ 了一下"小K",问 TA:"上周华东区的退货率是不是又涨了?"
两秒后,小K 回了一段带表格的分析报告。看起来 TA 像个真人助理 —— 但 TA 其实是一个 agent,一个有名字、有人设、有技能、有渠道账号的"数字员工"。
kweaver-dip 不是个"聊天机器人项目",而是企业级数字员工平台。它把 AI 抽象成一种"可治理的同事"——能上岗、能挂技能、能进飞书群、能被 HR 按规则管理。
数字员工的"四件套"
每个数字员工都由这四件东西定义。看完这一屏你会发现,"创建 AI 助手"这件事其实是 4 个独立可改的文件。
IDENTITY.md
名片。写着 TA 叫什么、什么岗位、头像 ID。改这个文件 = 改"工卡正面信息"。
SOUL.md
灵魂。写着 TA 性格如何、说话语气、价值观。改这个文件 = 给 TA 换一个"职业人设"。
Skills(技能包)
背包里装着的工具。比如"查 SQL"、"读 PDF"、"调用 BKN"。绑哪些技能,TA 就能干哪些活。
Channel(渠道)
飞书号 / 钉钉号。绑了之后,同事就能在群里 @ TA 而不是登平台后台聊天。
这四件套就是企业级 AI 应用的最小积木 —— 一句"我要新建一个数字员工" = 我要写 4 个不同的字段 / 文件。
代码里的"创建一个数字员工"
下面这段代码是 Studio 后端真正"造出"一个数字员工时跑的核心 5 步。来源:studio/src/logic/digital-human.ts 的 createDigitalHuman 方法。
public async createDigitalHuman(
request: CreateDigitalHumanRequest
): Promise<CreateDigitalHumanResult> {
const id = request.id?.trim() || randomUUID();
const template = buildTemplate(request);
const workspace = resolveDefaultWorkspace(id);
await this.openClawAgentsAdapter.createAgent({
name: id,
workspace
});
await this.writeTemplateViaOpenClawFilesRpc(id, template);
const skills = normalizeCreateDigitalHumanSkills(request.skills);
await this.agentSkillsLogic.updateAgentSkills(id, skills);
第 1 步:分配工号。 用户传了 id 就用,没传就生成一个 UUID——就像新员工入职 HR 给的工号。
第 2 步:渲染人设模板。 把请求里的 name / creature / soul 等字段渲染成 IDENTITY.md 和 SOUL.md 文档内容。
第 3 步:分配工位。 workspace 是 TA 工作时存文件的目录路径,类似公司给新员工分配工位。
第 4 步:在 OpenClaw 里"注册"这个员工。 OpenClaw 是底层的 agent 运行时,调它的 RPC 让员工在系统里挂上号。
第 5 步:写身份和灵魂文件。 把 IDENTITY.md / SOUL.md 通过 OpenClaw 的文件接口写进 TA 的工位。
第 6 步:装技能背包。 把绑定的技能列表登记好——TA 可以做哪些事就由这一步决定。
5 步全是"调用别人完成一件事"——这种"不做事,只编排"的代码在工程里叫 编排(orchestration)。Studio 是个调度中心,不是干活的人。
一次群聊看懂"@ 数字员工"
当你在飞书里 @ 一下小K,下面这些"角色"轮流发言。点 下一条 看完整剧情。
小测一下:你跟 AI 工具说话能不能更精准
这一节的 quiz 不考你"记住了几个名词",而是考你能不能用学到的概念准确指挥 AI 编码工具。
问题 1:你想把数字员工的岗位改成"合同律师",应该改哪份文件?
问题 2:同事说"小K 在飞书里没收到消息",下面哪个最先该排查?
问题 3:你要给 AI 编码工具下指令"加一个数字员工",下面哪句最精准、最不容易让 AI 理解错?
五大主角登场
一支乐队的五个声部 —— 每个声部用最适合自己的乐器,靠节拍合作。
为什么一个项目里挤了五种语言?
打开 kweaver-dip 仓库,根目录下你会看到 5 个完全不同语言写的子目录。乍一看像"团队混乱"——但这是有意为之。
不是合唱团(一种声音重复几十遍),是乐队——每个声部用最擅长的乐器:键盘、贝斯、鼓、主唱、和声。它们靠节拍(HTTP API + WebSocket 协议)合奏。
"鼓手" —— 节奏中枢。所有数字员工的对话流式编排都在这里。
"键盘手" —— 统筹和声。用户登录、Token、应用门户都归它管。
"贝斯手" —— 埋在底层却定义律动。把数据炼成业务知识,跑高并发管道。
"和声组" —— 织出审批规则。BPMN 引擎让流程随业务变。
"主唱" —— 你看见的脸。所有界面的根源。
仓库长什么样
下面是真实仓库根目录里值得你认识的 7 个子文件夹。每一个对应一个"舞台"。
monorepo 的意思是"多个项目共用一个 git 仓库"。kweaver-dip 在 monorepo 里塞了 4 种后端语言 + 1 种前端 —— 这种组合叫 polyglot monorepo。Google 用这种方式管几亿行代码。
看 Studio 是怎么"装乐器"的
来读一段真实的 Studio 入口代码,studio/src/app.ts 第 50-66 行。它一眼让你明白"调度中心"是什么意思。
export function createApp(options: AppOptions = {}): Express {
const app = express();
app.disable("x-powered-by");
app.use(express.json());
app.use(createHydraAuthMiddleware());
app.use(createHealthRouter());
app.use(createGuideRouter());
app.use(createBknRouter());
app.use(createCronRouter());
app.use(createChatRouter());
app.use(createSessionsRouter());
app.use(createSkillsRouter());
app.use(createChannelUserRouter());
app.use(createDigitalHumanRouter());
app.use(createChatUploadRouter());
app.use(createChatAgentRouter());
建立一个 Express 应用实例,叫 app。它就是要装乐器的乐器架。
关掉一个泄露技术栈信息的响应头(小小的安全细节)。
告诉 app:"我要解析 JSON 请求体"。
第一个挂上去的是中间件 —— 鉴权关卡:每一个请求先验 Token 才放行。
健康检查路由(让 K8s 知道服务还活着)。
引导路由(首次部署完成 OpenClaw 配置的引导)。
业务知识网络(BKN)转发路由 —— 把 Web 的请求转给 GBKN。
计划任务(cron)路由 —— 让数字员工定时干活。
聊天历史查询路由。
会话列表路由。
技能管理路由 —— 装/卸技能包。
渠道用户路由(飞书/钉钉用户名册)。
数字员工 CRUD 路由(创建/查询/更新/删除)。
聊天附件上传路由。
最关键的"@ 数字员工对话"流式路由 —— 第 3 章主角。
看见没——一个 17 行的函数挂了 14 个 router。这就是"调度中心"的样子:自己几乎不写业务,把请求按路径分配给负责的子模块。
看一段 Go 代码长什么样
不同语言写"读数据库"长得不一样。下面是 GBKN 用 Go 查询库表理解状态的最小例子,来源 gbkn/api/internal/logic/data_semantic/get_status_logic.go。
func (l *GetStatusLogic) GetStatus(req *types.GetStatusReq) (resp *types.GetStatusResp, err error) {
logx.Infof("GetStatus called with id: %s", req.Id)
// 调用 Model 层查询 form_view 状态
formViewModel := form_view.NewFormViewModel(l.svcCtx.DB)
formViewData, err := formViewModel.FindOneById(l.ctx, req.Id)
if err != nil {
return nil, errorx.Detail(errorx.QueryFailed, err, "库表视图")
}
resp = &types.GetStatusResp{
UnderstandStatus: formViewData.UnderstandStatus,
}
logx.Infof("GetStatus success: form_view_id=%s, status=%d",
req.Id, formViewData.UnderstandStatus)
return resp, nil
}
这是一个"查询库表理解状态"的函数。req 是入参,resp / err 是出参——Go 的函数可以同时返回多个值。
先打一行日志记录"我被谁、用什么 id 调用了"。
建一个 Model 层对象。Model 层是 Go-Zero 框架里专门跟数据库打交道的角色。
查数据库。FindOneById 顾名思义——按 id 找一条记录。
如果出错,返回一个带"上下文"的错误。Go 工程师习惯把 err 一层层包起来传上去,让调用方知道"在哪一层、为什么失败"。
把数据库里查到的状态字段塞进响应结构体。
再打一行成功日志。Go 项目对日志非常 verbose——这是工程纪律。
返回 resp 和 nil(nil 表示没出错)。
对比 Studio 的 TS 代码 vs GBKN 的 Go 代码——你能感觉到 Go 更"克制"、更"工业感"。这正是它适合干高并发管道的原因。
把语言和职责连起来
把 5 张技能牌(左)拖到 5 个职责(右)。在手机上长按然后拖动。
WebSocket 流式编排(数字员工对话)
OAuth 登录门户、用户应用列表
Kafka 高并发异步管道、知识图谱语义抽取
BPMN 工作流引擎、企业审批
用户界面、路由、表单
小测:定位你要让 AI 改的"舞台"
问题 1:用户报告"飞书里和小K 聊天,对话不返回" —— 你最先该 grep 哪个目录的代码?
问题 2:你想加"自动从 PDF 抽合同条款入图"功能,主体逻辑应该写在哪个仓库?
问题 3:你想加"4 级审批流程",BPMN XML 应该让 AI 改哪个仓库?
一句话的旅程
地铁换乘网 —— 一句话从你按回车到 AI 回复,要换乘 5 次、过 2 道闸机。
把它想成早高峰地铁换乘
你按下回车的瞬间,请求并不是"飞向 AI"。它要从 1 号线(Web 浏览器)出发,到 4 号线(Hub 鉴权)换乘验票,再换 10 号线(Studio 转流),最终到达终点站(OpenClaw + 大模型)。
对话不是一个 HTTP 请求就完事。第一步先 POST /chat/session 拿一个 sessionKey;第二步再 POST /chat/agent 用 sessionKey 开 SSE 流。这设计的好处:第一步纯 HTTP 易缓存,第二步是长连接。
完整数据流动画
下面这张动画追一句话从手指敲下到第一个字回来的全过程。点 下一步 看每一站发生什么。
看 Studio 怎么处理这一切
下面是 studio/src/routes/chat.ts 真实的对话路由。26 行代码就是"两步舞"的第二步。
router.post(
"/api/dip-studio/v1/chat/agent",
async (request, response, next) => {
const abortController = new AbortController();
attachDownstreamAbortHandlers(request, response, abortController);
try {
const requestBody = readChatAgentRequestBody(request.body);
const sessionKey = readRequiredSessionKeyHeader(request.headers);
const agentId = readAgentIdFromSessionKey(sessionKey);
const sessionLabel = await resolveChatAgentSessionLabel(
sessionsLogic, sessionKey, requestBody.message);
const message = appendAttachmentHintsToMessage(
requestBody.message, requestBody.attachments);
const upstreamResponse = await chatAgentClient.createResponseStream({
sessionKey, message,
attachments: requestBody.attachments,
idempotencyKey: randomUUID(),
sessionLabel
}, agentId, abortController.signal);
writeEventStreamHeaders(response, upstreamResponse.status, upstreamResponse.headers);
await pipeEventStream(upstreamResponse.body, response);
注册一条 POST 路由 /chat/agent。
建一个 AbortController——一旦下游断开,能让上游 fetch 也跟着取消。
绑定取消信号到 request/response 的 close 事件。
从请求体里读出消息内容("上周华东区退货率…")。
从请求头读出 sessionKey。这是上一步拿到的"两步舞门票"。
从 sessionKey 里反向拿出 agent id(也就是要找哪个数字员工)。
如果是会话第一句,给会话生成一个标签(用消息前几个字 + 随机后缀)。
如果消息附带文件,把文件路径作为隐藏上下文塞进消息里——这样大模型能看到。
关键的一行:调上游 OpenClaw,开始一条流式响应。idempotencyKey 是防重发用的随机 UUID。
把上游的 SSE 响应头转给浏览器(包括 content-type、cache-control 等)。
最后这一行是真正的"管道"——上游字节进来就直接写给浏览器,不等不缓存。
SSE 头部里藏着的"反 buffer"魔法
如果你曾经被一个"流式接口卡住"折磨过,下面这段 18 行代码可能就是答案。来源:studio/src/routes/chat.ts。
export function writeEventStreamHeaders(
response: Response,
statusCode: number,
headers: Headers
): void {
response.status(statusCode);
response.setHeader(
"content-type",
headers.get("content-type") ?? "text/event-stream; charset=utf-8"
);
response.setHeader(
"cache-control",
headers.get("cache-control") ?? "no-cache, no-transform"
);
response.setHeader("connection", headers.get("connection") ?? "keep-alive");
response.setHeader("x-accel-buffering", "no");
response.flushHeaders?.();
}
这个函数负责设置流式响应的 HTTP 头部——一字一句往浏览器吐之前先告诉它"接下来是流"。
设置状态码(一般是 200)。
告诉浏览器响应体是 text/event-stream——浏览器看到这个就知道"用 EventSource 接"。
告诉浏览器和中间代理"不要缓存、不要变换内容"——AI 流式输出最怕被缓存住。
长连接保持活着,别一秒没数据就断开。
魔法行:x-accel-buffering: no。这是给 Nginx 这种代理看的——意思是"这次别 buffer,请字节级转发"。少了这一行,SSE 在生产可能完全失灵。
立刻把头部冲出去——别等响应体凑齐才发。
90% 的"流式接口生产挂"都是 x-accel-buffering: no 没设、或者前面有一层代理(Nginx / Cloudflare / 公司网关)非要 buffer。下次让 AI 工具调试 SSE 时,第一时间检查这一行——你能省下几小时排查时间。
小测:给你 4 个"卡住不动"的场景
问题 1:一个聊天突然卡住没字符往外冒。下面哪个最不可能是原因?
问题 2:sessionKey 长这样 agent:foo:user:bar:direct:abc。其中 "foo" 是什么?
问题 3:为什么 Studio 在创建流之前要先生成 idempotencyKey?
问题 4:你改 Web 改了半天聊天卡住——但 Network 面板看到 /chat/agent 200 OK 而且每秒有数据进来。最可能问题在哪?
知识从数据中析出
金矿提炼车间 —— 从一堆带泥沙的矿石(库表字段)炼出可挂在墙上的成品(业务知识网络)。
把数据炼成知识,要过几道工序?
想象一张表叫 t_order_002_v3,里面 47 个字段名是缩写:"cust_id"、"amt"、"st"、"shp_fee"。AI 看一眼说"这是订单表"——但这个结论不能直接进知识图谱。
因为业务规则不允许"AI 说啥就是啥"。在 kweaver-dip 里,每张库表都得走一条 6 阶状态机,AI 只是其中一道工序,最终归属必须人审才入图。
"AI 辅助识别 + 人工审核 + 可回滚"是企业级 AI 应用和玩具应用的分水岭。下次你的 AI 编码工具说"我帮你做个全自动入库管道",记得问一句:"状态机呢?人审呢?失败回滚呢?"
6 阶状态机:库表理解的一生
这是来自 gbkn/model/form_view/vars.go 真实定义的 6 个状态。每一步都对应一个明确的"事件"。
初始状态。表刚接入,AI 还没看过——像新矿石刚到车间。
用户点了"一键生成"。Go 服务把字段打包,扔给 AI 服务异步处理——矿石进了第一道滚筒。
AI 出了候选业务对象("客户"、"订单金额"、"运费")。等人审决定 AI 猜得对不对——这是质检员上班时间。
人审通过,业务对象拍板。还可以再次重新理解(回到 1)——成品入库但还没上架。
真正进入业务知识网络(BKN)。下游 Agent / RAG 检索时能看到——上架的成品。
旁路。AI 调用挂了 / Kafka 拉不到消息 / 解析出错——状态会自动回退到这里,可重试。
代码里的"状态机闸门"
下面是 gbkn/api/internal/logic/data_semantic/generate_understanding_logic.go 一段惊人浓缩的代码——25 行里包含了"前置校验、防抖、状态过渡"三个企业级模式。
func (l *GenerateUnderstandingLogic) GenerateUnderstanding(req *types.GenerateUnderstandingReq) (resp *types.GenerateUnderstandingResp, err error) {
logx.Infof("GenerateUnderstanding called with id: %s, fields count: %d", req.Id, len(req.Fields))
// 1. 状态校验:只有状态 0(未理解)或 3(已完成)才允许生成
formViewModel := form_view.NewFormViewModel(l.svcCtx.DB)
formViewData, err := formViewModel.FindOneById(l.ctx, req.Id)
if err != nil {
return nil, fmt.Errorf("查询库表视图失败: %w", err)
}
currentStatus := formViewData.UnderstandStatus
if currentStatus != form_view.StatusNotUnderstanding && currentStatus != form_view.StatusCompleted && currentStatus != form_view.StatusFailed {
return nil, fmt.Errorf("当前状态不允许生成理解数据,当前状态: %d", currentStatus)
}
// 2. 限流检查(1秒窗口,防止重复点击)
if !l.svcCtx.AllowRequest(req.Id) {
return nil, fmt.Errorf("操作过于频繁,请稍后再试")
}
// 3. 更新状态为 1(理解中)
err = formViewModel.UpdateUnderstandStatus(l.ctx, req.Id, form_view.StatusUnderstanding)
if err != nil {
return nil, fmt.Errorf("更新理解状态失败: %w", err)
}
函数签名:接受一个"理解请求",返回响应或错误。
先打日志记录调用上下文(哪个表、多少字段)。
第 1 道闸门 · 状态校验。 先去数据库查这张表当前状态。
如果查询出错,直接返回。%w 是 Go 的"包装错误"语法——能保留底层 error 让上层追溯。
核心一行: 只有 0/3/5(未理解、已完成、失败)三种状态才允许重新生成。其他状态进来就拒。这就是状态机的精髓——不是 AI 想生成就生成。
第 2 道闸门 · 限流。 同一个表 1 秒内只允许触发一次生成——防止用户连点。
如果限流命中,直接拒。报错文案对用户友好。
第 3 步:提前置位。 在调 AI 之前先把状态置为 1(理解中)。
如果连状态置位都失败,立刻返回——后面就不要继续了。
注意第 3 步——为什么在调 AI 之前就把状态改成"理解中"?因为 AI 调用是耗时的,期间任何人来查"这张表在干嘛",都能看到"理解中"。这就是占位模式——并发场景下避免两个请求同时在炼同一张表。
如果 AI 调用挂了怎么办?
这是分布式系统最关键的场景:你已经把状态改成"理解中",结果 AI 服务返回 500。状态卡死了?不会——看这 5 行:
// 5. 调用 AI 服务 HTTP API(同步调用)
if err := l.callAIService(req.Id, formViewData, fields, len(req.Fields) > 0); err != nil {
// 调用失败,回退状态
_ = formViewModel.UpdateUnderstandStatus(l.ctx, req.Id, currentStatus)
return nil, fmt.Errorf("调用 AI 服务失败: %w", err)
}
注释说明:第 5 步去调 AI 服务(同步调用)。
if 判断:如果 AI 调用返回错误—— err 不是 nil。
注释一句"调用失败,回退状态"。
关键行:把状态从"理解中"改回 currentStatus(之前那个 0/3/5)。前面的下划线 _ 表示"我故意忽略这个返回值"——回滚本身的失败已经处理不了了,先尽力一试。
把外层错误包装好返回给前端。
这种"先占位、出错就回退"的模式有个正式名字,叫 补偿动作(compensating action)。当你的 AI 工具写"先 update DB、再调外部 API"的代码时,如果不加这个回退,就埋了一个"状态卡死"的坑。
车间群聊:一次完整冶炼
把上面的状态机配合 Kafka,串成一段群聊。点 下一条 看完整剧情。
小测:把"治理"塞进 AI 应用
问题 1:你点了"一键生成"按钮但没反应(显示"操作过于频繁")。这个限流的目的是?
问题 2:AI 服务返回失败时,代码做了什么?
问题 3:Kafka 在这个架构里扮演什么角色?
问题 4:你想让 AI 编码工具加一个"AI 自动跳过人审直接发布"的开关——一个有经验的工程师会怎么 push back?
工作流与审批
机场塔台调度 —— 业务流程像航班,按图纸走,塔台调度。
业务流程不是 if-else,是图纸
"我们这个合同要 4 级审批,第 2 级两人会签,法务部国庆节免审。"——你听到这种需求时,第一反应应该是什么?
错误反应:让 AI 写一堆嵌套 if-else。3 个月后业务说"加一级"——你得改代码、走发布、可能蹦回归测试一周。
正确反应:用一张图纸(BPMN XML)描述流程,让引擎(Activiti)读图执行。改流程 = 改 XML = 不动代码。
kweaver-dip 的 workflow 模块是配置驱动架构的极致案例。它把流程从代码里"挤出来"放进 XML,引擎按图办事——这是企业级软件能撑 10 年不重写的关键。
workflow 模块的三层叠塔
workflow 仓库内部其实是三个 Maven 子模块,各管一段。
REST API 与 Web 层。把外部 HTTP 请求转成内部调用、做鉴权、做异常处理。塔台对外的"窗口"。
核心业务逻辑 + Activiti 引擎集成。流程定义管理、任务分配、流程实例追踪。塔台里的"调度脑"。
文档审核管理模块。审核策略、审核人分配、第三方审核集成。塔台调度的"档案柜"。
REST API 的命名习惯
看看 workflow 暴露的几个核心端点。这些 URL 用的术语本身就在传授一种思维方式。
POST /api/workflow-rest/v1/process-definitions
PUT /api/workflow-rest/v1/process-definitions/{id}
POST /api/workflow-rest/v1/process-definitions/{id}/deploy
POST /api/workflow-rest/v1/process-instances
POST /api/workflow-rest/v1/tasks/{id}/complete
创建一个流程"模板"。 注意这个词 process-definition——这是 BPMN 里的"图纸",还没开始跑。
更新一个已有图纸(修订版本)。
把图纸部署到引擎。 部署 = 让引擎能识别它。没部署的模板永远跑不起来。
启动一个流程实例。 这是关键 ——process-instance 是模板的一次"具体跑动"。一份合同提交 = 启动一个实例。
完成一个任务。 流程跑到某一步会生成 task,分给候选审核人。完成 task 的接口就是这一行——审核人点"同意"按钮就是调它。
这是 BPMN / 工作流圈最重要的两个词。Definition 是图纸(一张),instance 是按图纸跑出来的航班(很多个)。下次你跟 AI 工具说"我要改流程",先想清楚是改图纸(definition)还是查某次实例(instance)——它会跟着你的精度走。
一次审批的 5 步生命周期
从画图到审完,一份合同要经历 5 个阶段。每一步都对应一个 API 调用。
业务专家用 BPMN 工具画出流程("经理审 → 总监审 → 法务审")。这一步不用写代码——产物是 .bpmn 的 XML 文件。
调 POST /process-definitions/{id}/deploy,Activiti 引擎读 XML,把节点表写进数据库。
用户提交合同 → 调 POST /process-instances 启动一次。引擎给这次执行分配实例 id,开始执行第 1 个节点。
引擎按图把 task 自动分配给候选审核人。会签时同时给多人,或签时只给一组里其中一个。
每个审核人调 POST /tasks/{id}/complete 同意/打回,引擎自动推下一个节点,直到流程结束。
企业审批的 4 种核心模式
下面这 4 张卡是 BPMN 引擎送给你的"自由功能"——你不用写一行代码,引擎已经帮你想好了。
会签
一个节点要多人都同意才能往下走。比如重要合同需要财务总监+法务总监一起签。引擎自动等齐所有人的"同意"。
或签
一组候选人,任一个先签就算通过。比如运维值班,5 个人轮流,谁先看见消息谁批。
委托
审核人忙不过来,把 task 转给同事代审。引擎记录"原审核人 + 代审核人",审计可追溯。
撤回
提交人发现填错了,在还没人审之前可以撤回。流程实例回到 draft 状态,不留审计噪音。
不用 BPMN 引擎,这 4 个模式每个都得你自己写代码。会签的并发计数、撤回的状态回退、委托的审计链——一个不小心就是 1000 行 if-else 加 3 张表。选对工具能省 80% 的代码。
小测:跟 AI 工具讨论审批需求
问题 1:客户说"4 级审批,第二级两人会签,法务部国庆免审"——下面哪个最不是 AI 编码工具能直接帮你写出来的部分?
问题 2:同事在 Web 提交了合同,但管理后台看不到这条审批——可能是哪一步出问题?
问题 3:一个工作流系统只用 if-else 写在 Java 代码里,不用 BPMN 引擎——它最大的麻烦是?
跨语言架构的"为什么"
多国合作建桥 —— 把每件事交给最对的工具,靠统一图纸协作。
为什么不"统一用 Python 重写"?
看完前 5 章你可能还有一个疑问:为什么 kweaver-dip 用了 5 种语言?工程团队是不是有点"任性"?
不是。这是一个深思熟虑的取舍 —— 业内叫 polyglot architecture。它的核心信念:用语言匹配场景,比用一种语言搞定一切更划算。
钢索工艺日本最强、混凝土技术德国最稳、水文勘测荷兰最熟、桥头雕刻意大利最美。统一让一个国家做反而更慢更贵——只要大家用同一份图纸(HTTP API + JSON)就能合作。
5 种语言,各擅胜场
TypeScript / Node
擅长:WebSocket 长连接、流式编排、事件循环。Studio 用它把 OpenClaw 的对话流转成 SSE 给前端——天作之合。
Python / FastAPI
擅长:OAuth 鉴权门户、快速原型、CRUD API。Hub 用它做登录 + 应用列表——半小时就能跑起来,新功能加得飞快。
Go / Go-Zero
擅长:高并发、Kafka 异步管道、内存效率。GBKN 每秒处理几千条 AI 识别消息——Go 的 goroutine 比 Python 线程便宜两个数量级。
Java / Spring Boot
擅长:BPMN 引擎、企业中间件、长生命周期项目。Activiti 是 Java 独有的成熟工作流引擎——其他语言的同类生态差了一个数量级。
React / TypeScript
擅长:复杂前端状态、组件复用、monorepo 共享。Web 子目录里 7 个不同的 React app 共享一套设计系统——这是 React + monorepo 的甜区。
两个不同语言,相同的目录结构
下面这件事最有趣:Studio(TypeScript)和 Hub(Python)虽然语言完全不一样,但目录结构几乎一样。这不是巧合,是六边形架构(hexagonal architecture)这种设计纪律的功劳。
# Studio(TypeScript)
src/app.ts # 组装 Express 应用、中间件和路由
src/server.ts # 读环境变量并启动服务
src/utils/ # 通用工具与运行时配置
src/infra/ # 基础设施适配层(OpenClaw 客户端)
src/adapters/ # 外部资源适配器
src/routes/ # HTTP 路由定义
src/logic/ # 核心业务逻辑
src/middleware/ # 通用中间件(404、错误处理)
src/errors/ # 领域内可复用的错误类型
# Hub Backend(Python)
src/
├── domains/ # 领域层:核心业务逻辑和领域模型
├── ports/ # 端口层:接口定义
├── application/ # 应用层:用例和业务编排
├── adapters/ # 适配器层:端口的具体实现
├── routers/ # 路由层(入站适配器)
├── infrastructure/ # 基础设施层:配置、日志、容器
└── main.py
看左边的 Studio(TS)。 它把代码分成 9 层,每层职责清晰。
核心层:logic(业务)、adapters(外部依赖适配器)、infra(基础设施)。这就是六边形的内圈。
边界层:routes(HTTP 入口)、middleware(横切关注点)、errors(错误类型)。这是六边形的外圈。
看右边的 Hub(Python)。 同样的分层逻辑,连命名都几乎一样。
domains = TS 的 logic(核心业务)。ports = 接口("插座")。adapters = 接口的实现("插头")。
routers = HTTP 入口(叫"入站适配器"),跟 TS 的 routes 一样。
关键收获:架构纪律是跨语言的。换语言不用扔掉这套思维方式——你跟 AI 说"按六边形分层",它在 Java/Go/Rust 都能给你做对。
跨语言的"普通话" —— 错误响应规范
5 种语言能合作,靠的是约定一套通用"格式"。kweaver-dip 整个仓库的错误响应都长这样:
{
"code": "DipStudio.SkillBadLayout",
"description": "SKILL.md is missing required front matter metadata",
"solution": "补充合法的 front matter,并确保包含 name 字段",
"detail": {
"upstream": {
"service": "openclaw",
"operation": "skills.install",
"httpStatus": 400,
"code": "BAD_LAYOUT"
}
},
"link": "https://example.internal/docs/errors#DipStudio.SkillBadLayout"
}
code 是稳定的业务错误码。 DipStudio.SkillBadLayout 这个字符串永远不变——前端 if 判断只能看它,永远别看 description。
description 是给人看的。 文案可能改、可能翻译——做 if 判断会很脆。
solution 是 UI 提示。 直接显示给用户的"该怎么办"。
detail.upstream 才是上游原始错误。 这里看得到底层是哪个服务、什么错。
关键纪律:上游的 BAD_LAYOUT 只在 detail 里——不能直接当 Studio 的对外错误码。否则换上游服务,前端代码全爆炸。
link 字段给跳到帮助文档。
"如果错误描述里包含'权限'两个字就跳登录页"——这就是看 description 写逻辑的坏习惯。文案一改就全坏。永远用 code 字段做 if 判断。这条纪律救过无数线上事故。
谢幕群聊:5 种语言的协奏
课程的最后一段群聊:你提交一份合同审批,5 种不同语言的服务在 iMessage 群里"用 HTTP/JSON 普通话"协作。这是给整个课程的收尾。
最后小测:你已经能跟工程师对话了
问题 1:客户说"加一个高并发文件入库管道,每秒 5 万条"——AI 工具建议你用 Python multiprocessing。你应该 push back 改用什么?
问题 2:你想给 hub 的登录接口加一个新的 OAuth provider(Lark)。下面哪个改动最像 hexagonal 架构的"端口/适配器"做法?
问题 3:错误响应里同时有 code: "DipStudio.SkillBadLayout" 和 detail.upstream.code: "BAD_LAYOUT"。前端 if 判断应该看哪一个?
问题 4:同事问你"为什么 kweaver-dip 不全用 Python 重写更省事?"你的最佳回答是?
收尾的话
6 章下来,你认识了数字员工的"四件套"、5 种语言各自的角色、一句话穿过 5 站的旅程、知识从数据中析出的状态机、BPMN 工作流引擎,以及为什么这一切要拆开成 polyglot 架构。
下次你跟 AI 编码工具聊企业 AI 系统时,你能用"sessionKey"、"补偿动作"、"process instance"、"hexagonal port/adapter"、"polyglot"这些词精准沟通。它会因此少走很多弯路 —— 而你也会因此从"vibe coder"逐渐变成"会驾驭 AI 的工程师"。
把今天学到的拿去用 —— 改你自己的项目。看你的项目用了多少这些模式,缺了哪些。这才是这门课真正的"作业"。