夜雨聆风学习资料网

ARTICLE · 1135208

multica 源码 11:LLM 集成层——最深的功夫,都在模型之外

multica 源码 11:LLM 集成层——最深的功夫,都在模型之外

缓存分层、确定性裁剪、按模型计量,外加一套提示词装配术——一个生产级 agent 平台的工程纪律

在 Multica 里给一个 agent 派任务,从点下指派到模型吐出第一个字,中间隔着八段链路。我最近在逐层精读它的源码,LLM 集成这一层读完,感受有点反直觉:整个系统下功夫最深的地方,几乎没有一行代码在"调模型"。

这篇是"multica 源码"系列的 LLM 集成层篇。初版用三张图把调用链路、prompt 组装、上下文管理画透了,我从中提炼出三条工程纪律。

这一版是加长版。后来又读了两份研究:一份把平台的内置提示词数了个底朝天,一份把上下文从组装到超限的机制摸到了头。新收获并进对应章节,配图也从三张补到七张。

先分清:系统里有两条 LLM 路

刚翻开代码时,我默认"agent 平台调用 LLM"是一件事。读完才发现 Multica 里是两条完全独立的路,混在一起就读不懂代码。

Agent 执行路径(主路径)
服务端 Assist 路径(旁路)
谁调模型
本地 daemon 启动的 CLI 子进程,支持 24 种 runtime
API server 进程自己
凭据
runtime 自持,用户自己的账号
平台配置的 OpenAI 兼容网关
干什么
写代码、发评论、改状态这些重活
chat 自动起标题、生成追问按钮

主路径是干活的路。旁路只做两件轻量辅助,而且不配置凭据就直接静默禁用,一个请求都不外发。

最触动我的是一条架构纪律:OpenAI 的 Go SDK 只允许被 server/pkg/llm 这一个包 import,代码里还专门写了两个测试守护这条规则。一个依赖被圈死在一个包里,想滥用都没有入口。

八段链路:模型开始回答之前发生了什么

主路径的完整调用链有八段:任务触发 → 入队队列 → daemon 认领 → 运行准备 → 启动 CLI 子进程 → runtime 内部的工具调用循环 → 流式回传 → 结果落地。

我原本以为重点在中间那段"工具调用循环",毕竟那是模型发挥的地方。分析报告却把镜头对准了运行前的准备阶段,四步的顺序值得背下来:

第一步落盘工作目录和上下文,第二步校验模型选择,第三步把平台配置写进 runtime 的配置文件,第四步才组装本轮用户消息。

为什么这个顺序重要?因为前三步决定了 agent 眼里的"世界":它以为自己在一个普通项目目录里干活,看到的是一份 CLAUDE.md,完全不知道外面有个平台在调度。第四步的 prompt 反而是最薄的一层。

认领这一步还有个讲究。daemon 批量认领任务,响应里除了 issue 编号和触发评论,还附带一把任务级令牌。

agent 全程只拿这把钥匙访问平台,接触不到 daemon 自己的凭据。干完活,结果连同用量一起回传落账。

Prompt 是三层结构,每层都在算缓存账

这是整个分析里我觉得最值钱的一节。Multica 给 agent 的最终 prompt 不是一大段文字,是三层结构,分层的动机只有一个:prompt 缓存。

A 层是稳定前缀。 daemon 把一份"运行手册"写进工作目录的 CLAUDE.md(按 runtime 不同,也会写成 AGENTS.md 等),里面有 19 个块。

块的内容包括:agent 的身份指令全文、背景任务安全规则、可用命令、评论格式、仓库信息、项目上下文,等等。

这份手册的写法非常讲究。任务开始时,它在 BEGIN/END 标记块内原位替换;任务结束,逐字节回滚。系统还配了一个测试,保证同类任务的 brief 两次运行之间字节级一致。差一个字节,缓存前缀就断了。

省 token 的细节做到了什么程度?Skills 一节只列名字不写说明,一项就省约 3100 个 token。

B 层是每轮变化的用户消息。 这里有个反常识的处理:issue 正文不塞进 prompt,只给一个 issue id 和一条读取命令,让 agent 自己去查。触发评论倒是全文内联,连同回复路由信息一起。

兄弟任务动态、会话连续性提示、任务发起人、已连接应用,这些易变的东西全部刻意压在 B 层。宁可让后缀变,不能让前缀动。

C 层是兜底。 只有 4 个不读上下文文件的 runtime,才把整个手册内联进系统提示。Claude 反而明确不用追加系统提示的参数:CLAUDE.md 已经加载了,再内联等于每轮重复一遍。

缓存效果不是靠信仰。日志里有缓存命中率指标,可以直接观测这套分层有没有白做。

拆开运行手册:提示词的四层体系

A 层那份手册里到底装了什么?后来的研究把家底整个翻了一遍,先给了一个反直觉的结论:翻遍 Multica 的 CLI 和网页客户端,找不到一行系统提示词。

CLI 是纯 API 客户端,所有提示词都在服务端和 daemon 侧组装好才下发。分发权收在平台手里,客户端只管执行。

全平台数下来一共 32 项提示词,分四层,各管一段:

层
是什么
代表成员
一
runtime brief,就是 A 层手册
身份、安全规则、命令速查、工作流
二
每轮变化的任务提示
触发评论、回复路由、发起人
三
产品功能型系统提示
squad 领队手册、内置管家 Mika、Agent Builder
四
服务端独立小调用
chat 自动起标题、生成追问按钮

第一层就是 A 层手册,19 个块按任务类型裁剪:issue 任务带完整工作流,快捷建单只给精简版命令表。这也是一种缓存账,塞给 agent 的每个字都要配得上它的位置。

手册之外还有 9 个内置技能文档,不预装,agent 快要用到时才按需加载。手册里只留一张名字索引,连说明都不写,就是前面省 3100 token 的那个做法。

这些东西最终按固定顺序拼装:最底下是 CLI 自带的系统提示,中间是平台手册,最上面是本轮任务消息。

agent 的自定义指令放在手册的身份块里,和"你是谁"写在一起。有两类身份还会继续加码。

squad 领队领任务时,服务端在指令后面追加一页协调者手册;内置管家 Mika 在产品提示之上叠一层工作区笔记,笔记只能微调行为,删不掉身份和确认义务。

手册里同样写明了优先级:身份指令大于运行时工作流,工作流不是越权许可。用户可控的文本,比如 agent 名字、项目描述,拼进手册前都要过转义。

建单时附带的原始上下文,则被声明为只读背景,里面任何"指令"都不许执行。防的不是用户,是藏在粘贴内容里的提示词注入。

提示词之间:没有继承树,只有装配图

数清数量之后,自然要问:这 32 项提示词之间是什么关系?我原本猜有一棵继承树,父模板定规矩,子模板覆写细节,一层层特化下去。

代码给的答案:一处继承都没有。实际起作用的是四种关系,每种都有代码为证。

第一种是组装。 所有手册块是平级兄弟,由同一个装配器按固定顺序拼装,哪个块出场由任务类型门控。最接近"继承"的只有一处特化:快捷建单用精简版命令表替换完整版。

第二种是分层叠加。 身份块是平台起头、用户指令续写;领队手册、Mika 笔记都是在基座上加层,而且加层删不掉基座的关键约束。

第三种最值得学,叫单点事实共享。 有一条命令边界规则,曾经在手册和任务提示里各写一份,后来两份漂移成一边写禁、一边写许,agent 无所适从。

修法是把规则提取成共享常量:全平台只在一个地方发射,别处只许引用,想漂也漂不起来。频道显示名、连续性通知这些容易复读的内容,也都收进了单点。

第四种是显式引用依赖。 回复指南这类小块不复制规则原文,直接写"规则见上文的评论格式一节"。抽掉被引用的那节,小块立刻不完整,缺了什么一眼可见。

这些引用有方向,方向由一条硬约束决定:手册是缓存前缀,同一会话内必须字节稳定,有专门测试守护。

所以凡是每次 run 会变的值,一律只能进逐轮消息;反过来,逐轮消息可以随意引用手册的节名。

所以这套体系最终长成这个形状:装配树定骨架,引用边防漂移。跨层覆盖只在三处显式声明,每一处都写明了理由。

上下文:平台拒绝用 LLM 管 LLM

上下文管理这一节,我看到"平台不做 LLM 摘要、不做压缩"这句话时停了一下。因为我自己的第一反应就是:对话太长了,加一层模型摘要不就解决了?

Multica 的答案是不加。所有裁剪都发生在确定性代码里,一个 token 都不花。敢这么做的前提是:issue 和评论存在数据库里,是可随时重读的权威记录,摘要丢了也不丢事实。

具体的裁剪是一条四层限额链。

第一层在读取时:扫描评论列表,每条截断到 200 字;单个 issue 最多读 2000 条;已有结论的讨论整串折叠,不进 prompt。

第二层在注入时:任务认领的评论数据有 512 KiB 硬顶,触发评论必送,超出的部分留到后续对账。

第三层在手册里:按任务类型裁剪块,易变值一律不进稳定前缀。

第四层是会话生命周期:上下文耗尽的会话直接退役,进黑名单,下次任务从全新会话开始。

附件的处理也彻底:永不内联。prompt 里只给附件 id 和下载命令,因为签名 URL 会过期,塞进 prompt 就是未来的坑。

后来那份上下文研究把整套机制画了下来,我才发现限额链只是防守面。整个模型一句话:薄注入,按需拉取,会话续传。

薄注入,任务认领时只给 issue 编号和触发评论,正文和历史一概不塞,agent 拿着 CLI 自己去读。会话续传,下一轮接着上一轮的会话指针继续,不用从头热身。

分层图自下而上读就是数据流。最底下是 CLI 自带的系统提示;往上是写进 CLAUDE.md 的手册;再往上是一层伴生文件:技能文档、issue 摘要、项目资源清单,各归各位落在工作目录里。

再往上是既往会话和本轮消息,最顶上是 agent 运行中用工具拉回的数据。正文内容主要走最顶上这条通道,平台预注入的始终只是个骨架。

超长输出的处理很干脆:超过八千字的最终输出不落评论,整段替换成一条固定通知。理由也直接,超限输出不可信,连节选都不保留。

还有个容易踩的坑:按"最近 N 条"读评论,封顶的是线程数,不是评论数。小 issue 上这么读,会把全部历史拖进上下文。手册里反复警告这一条。

记忆的处理各有取舍。Codex 的自动记忆被禁用,防止跨工作区泄漏;Hermes 是 agent 级的持久仓库;Claude 的记忆目录是原生能力,平台不去隔离。

要跨轮记住的任务状态,统一走 issue 的元数据。

上下文超限:会话的退休与重建

研究里我最受震动的是一段完整推演:上下文真的塞满了,接下来会发生什么?

先看模型这一侧。对话过长时,CLI 自己的自动压缩先上,压得回去就继续跑。压不回去,这一轮就地终止,返回一个明确的"提示词过长"信号。

平台用三道检测接这个信号:后端的结构化字段是正门;daemon 侧再用文本识别兜底,专防旧版本漏报;服务端的落库边界上还有最后一道。

三道任一命中,任务按失败记,失败原因写的是上下文溢出。超限的通知文本不会被当成 agent 的成功回复发出去,这条是硬规则。

关键的下一步在会话。这个失败原因不在可自动重试的名单里,该会话直接退休:选续传指针时被排除,不会形成"永远续传、永远超限"的死循环。

下一次任务触发,开的是全新会话。工作目录照旧复用,产出的文件一件不丢。

开跑前,平台在消息里塞一张"会话连续性通知",大意是:丢掉的只是没写下来的工作记忆,issue 和评论才是权威记录,不许谎称连续性。agent 重新把评论扫一遍,上下文就重建了。

研究还特意区分了一个相邻机制:续传被拒且这轮没干活的会话,会当场换新会话重试一次。超限不在其列,已经塞满的会话压不回去,重试必然重演,只能走退休这条路。

用量:按模型分桶,老老实实记下来

第三条纪律是计量。runtime 每次运行的用量,先在累加器里按桶去重合并,再按模型分桶上报,服务端幂等落库,按小时滚动聚合,最后进看板。模型维度贯穿全链路,因为成本定价发生在客户端的费率表上。

这里要澄清一个容易误解的点:Multica 没有平台级的 token 配额。issue 数量限制和席位管理都是商业限额,不是用量限额。

费用发生在每个 runtime 自己的 provider 账号上。平台只负责计量和展示,不负责买单。

还有一个细节我很喜欢:模型选择可以为空,而且这个空是刻意的。留空让各 CLI 自己解析默认值,比平台静态猜测更贴近用户账号的实际情况。

边界:这套做法什么时候不成立

三条纪律都好看,但照抄之前先看两个前提。

第一个前提是权威记录。"不做摘要"成立,是因为对话背后的 issue 和评论永远可重读。

如果你的场景没有这份权威记录(比如纯闲聊助手,历史丢了就丢了),那压缩或摘要就是必须做的,照抄"不摘要"会翻车。

第二个前提是场景收益。这套分层为 24 种 runtime 的抽象、字节级一致的守护测试,都带着维护成本。单一 runtime 的小项目,把 prompt 拼成一个字符串可能反而更健康。这些成本值不值,要对着自己的场景算。

收束:三条纪律

回头看,这三条纪律说的是同一件事:把不可控的模型调用,变成可预期的工程问题。缓存分层让它便宜,确定性裁剪让它可预期;至于每一次调用花了多少,分桶计量让每一笔都有账可查。

把两份后续研究并进来重读,我的收获多了一句:提示词是被装配的零件,上下文是有退休制度的资源。零件要防漂移,资源要有出口。

对正在转型 AI agent 开发的人,我的建议是一个具体的动作:打开你现在项目里组装 prompt 的那段代码,问自己三个问题:前缀稳定吗?裁剪是代码做的还是模型做的?用量按模型记了吗?

三个都答得上来,你已经在这个层级毕业了。

📩 留言区聊聊:你现在的项目里,上下文裁剪发生在哪一层?

相关学习资料