系列收官。前九篇我们沿着「一条消息的旅程」纵向穿透了从平台到流水线、从模型到沙箱、从 API 到持久化的每一层。但有一些东西不属于任何单独一层,而是横向贯穿所有层——可观测性、国际化、配置、RAG、安全。这一篇把它们收齐,最后给你一张「想改某个功能该从哪入手」的改码地图,让整个系列闭环。

一、什么是「横切关注点」

回看第一篇的分层图:Platform → Pipeline → Provider,旁挂 Plugin/Box/Persistence/API。这些是纵向的功能层。而下面这些能力,每一层都要用到、却又不该塞进任何一层的业务逻辑里:

flowchart LR
    subgraph layers["纵向功能层"]
        direction TB
        P1["Platform"]
        P2["Pipeline"]
        P3["Provider"]
        P4["Persistence / API"]
    end

    OBS["可观测性<br>monitoring + telemetry"]
    I18N["国际化 i18n"]
    CFG["配置 instance_config"]
    SEC["安全 鉴权/脱敏/沙箱边界"]

    OBS -.贯穿.-> layers
    I18N -.贯穿.-> layers
    CFG -.贯穿.-> layers
    SEC -.贯穿.-> layers

这些就是横切关注点。LangBot 处理它们的方式,恰恰最能体现它「toC + toB 双场景」的工程取向。

二、可观测性:监控与遥测是两件事

LangBot 把「可观测性」拆成两条互不相关的线,理解这个区分很关键:

<strong>Monitoring(监控)</strong> <strong>Telemetry(遥测)</strong>
数据归谁 留在<strong>本地数据库</strong> 匿名上报到 LangBot Space
看什么 这个实例自己的消息、查询、错误 跨实例的功能<strong>采纳率</strong>统计
谁看 部署者(Web 面板) 项目维护方
可关闭 — <code>space.disable_telemetry</code>

2.1 Monitoring:埋点贯穿流水线与 Provider

监控数据在「主线剧情」中被反复写入。回顾第四篇:RuntimePipeline.process_query() 在查询开始/成功/出错时都会记录监控;第五篇:RuntimeProvider 把每次模型调用的耗时、token 用量写进监控。这些记录最终落到 monitoring_messages 等表。

为了不让监控数据无限膨胀,第二篇提到的 monitoring-cleanup 后台任务会定期调用:

# src/langbot/pkg/api/http/service/monitoring.py:23
async def cleanup_expired_records(self, retention_days: int, batch_size: int = 1000) -> dict[str, int]:
    """Delete monitoring records older than the specified retention period."""
    if retention_days < 1:
        raise ValueError('retention_days must be >= 1')
    # ... 按表分批删除早于 cutoff 的记录 ...

默认保留 30 天、每小时检查一次(可经 monitoring.auto_cleanup 配置)——这正是「toB 场景下长期运行不爆库」的考量。

2.2 Telemetry:匿名心跳,只报「形状」不报「内容」

遥测心跳(第二篇的 telemetry-heartbeat 任务)启动后延迟 30 秒发一次、之后每天一次:

# src/langbot/pkg/telemetry/heartbeat.py:119
async def heartbeat_loop(ap: core_app.Application) -> None:
    """Send one heartbeat shortly after startup, then daily."""
    # Small delay so managers (platform, skills, plugins) finish loading first
    await asyncio.sleep(30)
    while True:
        try:
            payload = await build_heartbeat_payload(ap)
            await ap.telemetry.start_send_task(payload)
        except Exception as e:
            ...
        await asyncio.sleep(HEARTBEAT_INTERVAL_SECONDS)  # 24h

build_heartbeat_payload 收集的全是配置类别与对象计数——数据库类型、向量库类型、Box 后端、各类资源数量、适配器类型名(不是账号):

# src/langbot/pkg/telemetry/heartbeat.py:84
features['pipeline_count']       = await _count(ap, persistence_pipeline.LegacyPipeline)
features['mcp_server_count']     = await _count(ap, persistence_mcp.MCPServer)
features['knowledge_base_count'] = await _count(ap, persistence_rag.KnowledgeBase)

文件头的 docstring 把边界写得很死:

The snapshot contains only configuration categories and object counts — never names of user resources … never message content, never credentials.