一个域一份 prompt 的 Agent,我做到第二个域就接不下去了
多域 Agent 落地记录 · 01
这是我把手上这个保险 Agent 从设计做到落地的过程记录。走对的和踩坑的都写,不保证更新频率,但会一直写下去。
先说背景,免得看到一半发现不是你要的东西。
我在做保险业务的 Agent,要覆盖四个业务域:财务、续期、保全、消费者保护。不是写个 demo 发朋友圈那种,是真准备给业务组用的。
第一版朴素得有点寒酸:一个 agent loop,一份 system prompt,四个工具。财务域先跑起来了,效果还行,我当时还挺得意。
然后要接续期。我在这卡了两天。
代码在这儿,MIT,拿去改就是: github.com/a2819778558...
第一版我写了什么
大概是这样的:
erlang
你是财务助手。
当用户查询欠费时,先调用 queryArrears 拿到欠费期数,
如果期数 > 1,再调用 calcLateFee 计算滞纳金,
把两个结果合并成表格返回。
用户问报销进度就调 queryReimbursement。
看着挺合理对吧。单域场景确实能用,我甚至还给它加了几个边界条件的说明。
当时我的想法是"把流程写清楚,模型就不会乱来"。这个想法在单域的时候是对的。
第一次崩:prompt 越写越死
那天有个测试同事随手问了一句:
我这张保单欠了两期,滞纳金算一下,另外我上个月的报销单什么状态?
模型的回答是:滞纳金算得挺对,报销单没提。
我回去看,问题很清楚------我 prompt 里写的那条流程是"查欠费 → 算滞纳金 → 返回",里面根本没有"顺手回答另一个问题"这一步。模型很听话地走完了我画的路线,然后收了。
我第一次修是加一条规则:"如果用户同时问了多个业务域的问题,要分别处理。"后来发现得加第二条、第三条......最后那份 prompt 变成了一本规则手册,写满了各种 if-else 的自然语言版本。
而且它还是不好用。 规则越多,模型越容易顾此失彼。
第二次崩:加一个域要动内核
这个比上一个严重,因为它不是效果问题,是结构问题。
我要加续期域,实际操作是:
- 复制一份写死的 prompt,把"财务"改成"续期"
- 复制一套工具注册的代码
- 在 agent loop 里加几个
if (domain.equals("renewal"))
加一个域,我得改核心代码。
这意味着后面三个业务组(续期、保全、消保)没办法各自独立地往里填东西------每加一个域都得我来改一遍。四个组排队等我一个人。
到这儿我才决定返工。
我原来把"域"理解错了
返工前我想了很久,发现根源是一个挺蠢的误解。
我一开始默认"域"是执行边界:这个问题属于财务域,就用财务域的工具、跑财务域的提示词。
但用户不按你的组织结构提问。
开头那句话就是例子------「保单下个月到期,想用现金价值续一下,顺便看看有没有欠费」。这一句里有到期和续期(续期域)、现金价值(保全域)、欠费(财务域)。三个域同时在线。
如果域是执行边界,这句话就无处安放。你会被迫选一个"主域",然后另外两个域的能力直接被砍掉。
所以域这个东西,本质上是**"谁负责维护什么"的管理单位,不是"这个问题归谁执行"的边界**。
想通这一点之后,代码改动其实很小:路由的输出从一个域变成一个域集合。
java
public record RouteResult(
Set<String> domains, // ← 唯一的实质改动:集合,不是单个
String primaryDomain,
double confidence,
boolean needsClarification,
String reason // 保险场景要可解释,得写进审计日志
) {
public static RouteResult askClarification(String reason) {
return new RouteResult(Set.of(), null, 0.0, true, reason);
}
}
这句话改完,跨域问题就能装配出多个域的工具并集了。原来那句"顺便看看有没有欠费"从"被丢掉"变成"顺手就答了"。
现在的结构
┌─────────────────────────────────────────┐
│ 平台层(域无关,做一次,四域共用) │
│ AgentLoop · DomainRegistry · Assembler │
├─────────────────────────────────────────┤
│ 路由层(意图 → 域集合) │
├─────────────────────────────────────────┤
│ 域层(每域一份配置,可插拔) │
│ 财务 │ 续期 │ 保全 │ 消费者保护 │
└─────────────────────────────────────────┘
每个域都实现同一个接口,这就是"加一个域内核一行都不用改"的全部内容:
java
public interface DomainPlugin {
String name(); // "finance" / "renewal"
String displayName(); // "财务"
List<IntentSpec> intents(); // 这个域负责回答哪些意图
List<ToolSpec> tools(); // 只放业务动词工具
String systemPrompt(); // 只写角色 + 边界
default PermissionPolicy permissions() { // 同一域内不同工具可要求不同权限
return PermissionPolicy.allowAll();
}
default FallbackPolicy fallback() { // 答不了时转给谁
return FallbackPolicy.toHuman("客服");
}
}
一个域最少只要交出三样:意图表、工具、系统提示。权限和兜底有默认实现,不写也能跑。
接 Spring 更省事,各域实现类标个 @Component,平台侧注入 List<DomainPlugin> 就自动全收齐了,注册代码都不用写。
跑起来是这样的
我不太喜欢拿单域问答演示 Agent,那看不出结构好坏。跨域问题才是分水岭。
这是真实输出,我直接贴上来的:
ini
已注册的域:
- finance 财务 意图 3 个, 工具 3 个
- renewal 续期 意图 2 个, 工具 2 个
- servicing 保全 意图 3 个, 工具 2 个
- protection 消费者保护 意图 2 个, 工具 1 个
================ 用户:我这保单下个月到期,想用现金价值续一下,顺便看看有没有欠费
路由结果:domains=[servicing, renewal, finance], primary=servicing,
confidence=1.00, 需澄清=false
决策原因:关键词命中: [servicing, renewal, finance] (→ 写审计日志)
装配工具(7):[servicing.tryCashValue, servicing.surrender,
renewal.queryStatus, renewal.calcPremium,
finance.queryArrears, finance.calcLateFee,
finance.queryReimbursement]
→ 跨域取数:
[保全] servicing.tryCashValue => {现金价值=12450.3, 试算日=2026-10-01}
[续期] renewal.queryStatus => {下期到期日=2026-10-20, 宽限期至=2026-12-20}
[财务] finance.queryArrears => {欠费期数=2, 欠费金额=3860.0, 状态=已逾期}
一句话命中三个域,装配出 7 个工具的并集,然后跨三个域取数。
说清楚一点,免得有人以为我在吹:上面第 4 步"跨域取数"是写死的占位实现,真实项目这一步由 LLM 决定调哪几个工具。前 3 步(注册 → 路由 → 装配)是真跑的逻辑。
顺带一个踩出来的经验:工具别暴露 CRUD
这条是我实际踩过的,写在 ToolSpec 的注释里:
erlang
✅ servicing.surrender 办理犹豫期退保(业务规则封在工具内部)
✅ servicing.tryCashValue 试算现金价值
✅ finance.queryArrears 查询欠费明细
❌ updatePolicyField 改字段
❌ execSql 裸 SQL
原因很直接:写操作出事就是事故。
给模型一个 updatePolicyField,它就会很自信地往一张不该改的表里写值,而且你还拦不住。但如果给的是 servicing.surrender,那么犹豫期校验、状态检查、退款计算这些规则全都封在工具内部,模型只能决定"要不要办退保",决定不了"退保该怎么办"。
粗一点的经验是:读和查询可以粗,写和动作必须细。
还有一个不是代码的事
我做了份 docs/域接入登记表.md,是给业务专家填的,不需要懂代码。
里面最要紧的是意图表:
| 意图 ID | 意图名称 | 类型 | 用户可能的问法 | 需要哪些工具 |
|---|---|---|---|---|
| finance.arrears | 查询保单欠费明细 | tool | 我这张保单有没有欠费;欠了多少钱;还剩几期没交 | finance.queryArrears |
| renewal.premium | 计算应缴保费 | tool | 这一期要交多少钱 | renewal.calcPremium |
| ... | tool/rag/chat/handoff |
这张表同时是三样东西:路由做向量匹配的依据、评测 golden set 的骨架、业务和开发之间的接口。
我最初的版本没有这张表,意图是散在代码和 prompt 里的。后来发现不行------意图表里的 examples 必须是真实用户怎么说话,这东西只有业务专家知道,我和模型都编不出来。
现在什么还不行
- 路由器是最简的关键词实现。真实项目要换成「规则 → 向量 → LLM 结构化分类 → 澄清」的级联。我正打算换。
- 没有评测集。意图表里的 examples 只能算骨架,真实问法得几百条才够量准域准确率、跨域召回率和澄清率。
- 工具并集到 50 个以上就不能这么干了。扩展点想好了(工具 RAG 或渐进加载),但现在还没到那个量级,先不做。
- Memory 层还没接。跨轮次上下文归属到哪个域,是下一个要处理的。
下一篇要做什么
想通"域是域集合"之后,下一个撞上来的就是路由器。
现在这个 KeywordRouter 是我拿来演示的,就是个关键词 contains 匹配,真实场景跑不了。下一篇打算换成级联实现:规则先挡一批,剩下走向量,向量也不确定的丢给 LLM 做结构化分类,再不确定就直接反问用户澄清。
这里我预计会掉坑:级联每一层的阀值怎么定,没有评测集的话就是拍脑袋。所以再下一篇应该是搭 golden set,拿几百条真实问法把域准确率、跨域召回率、澄清率量出来。
工具并集和 Memory 归属这两个先放着,没到量级。
当前进度
arduino
2026-10-03
✅ 域插件契约定稿(DomainPlugin / ToolSpec / IntentSpec)
✅ 路由输出改成域集合,跨域问题装配工具并集
✅ finance 完整样板 + 另三个域骨架
✅ 域接入登记表(给业务专家填的)
⬜ 路由器换级联实现 ← 下一篇
⬜ 建 golden set,量准三个指标
⬜ 工具 RAG / 渐进加载(工具超 50 个再说)
⬜ Memory 层接入
⬜ 逐域接入生产
骨架地址(MIT):github.com/a2819778558... mvn compile exec:java 就能看到上面那段跨域输出。