
1. 金融场景下的智能协作工作台从零搭建一套可复用的 Managed Agents 方案金融行业的技术团队这两年有个很明显的感受业务侧对“智能助手”的期待值被拉得极高但真正落到生产环境里能跑通、能审计、能复用的方案少之又少。我所在的团队从去年开始折腾一套面向金融业务的智能协作工作台核心目标很朴素——把散落在各个业务线里的重复性分析、文档处理、数据核对工作收敛到一个统一的、可管理的智能代理层上。这套东西我们内部就叫它 financial-services 工作台底层依托的是 Claude 系列模型能力配合 Cowork 的协作理念和 Managed Agents API 做代理编排再通过 plugin 机制做能力扩展。说白了它解决的是三个具体问题第一金融业务里大量非结构化文档研报、合同、对账单、尽调材料需要快速提取关键信息人工做又慢又容易漏第二多个代理之间需要协同工作比如一个负责抓数据、一个负责做合规校验、一个负责生成报告它们之间怎么通信、怎么管理生命周期第三金融场景对可追溯性要求极高每一次模型调用、每一个代理动作都得留痕不能是一笔糊涂账。这套方案适合有一定工程基础的金融科技团队也适合想了解 Managed Agents 落地路径的开发者参考。下面我把整个设计思路、关键实现和踩过的坑完整拆一遍。2. 整体架构设计与方案选型逻辑2.1 为什么是 Managed Agents 而不是自己造轮子一开始我们评估过两条路一条是完全自研代理调度框架另一条是基于 Managed Agents API 做上层封装。自研的好处是可控性拉满坏处是工作量巨大——你得自己处理代理的注册、发现、心跳、重试、状态持久化、并发控制这些在金融场景里一个都不能少。我们算过一笔账光是做一个能稳定运行的代理生命周期管理器至少需要两个资深后端投入一个季度还不算后续的运维成本。Managed Agents API 的价值在于它把这层脏活累活接过去了。代理的创建、销毁、状态查询、任务分发都有标准接口我们只需要关注业务逻辑本身。更关键的是它在设计上天然支持多代理协作代理之间可以通过消息机制传递上下文这对于我们那种“抓取-校验-生成”的流水线式场景非常契合。当然选它也有代价比如某些底层行为不够透明出问题的时候排查链路会变长这个后面会细说。2.2 Cowork 协作模式在金融场景的适配Cowork 这个概念听起来有点虚落到我们实际场景里其实很具体。金融分析往往不是一个人能完成的对应到代理层面也一样。我们设计了三类角色代理采集代理负责从各种数据源拉取原始材料分析代理负责做指标计算和风险识别报告代理负责把结果组织成人类可读的格式。这三类代理不是简单的串行调用而是通过一个共享的上下文空间做协作。举个例子采集代理拿到一份对账单后会把解析出的交易记录写入共享上下文分析代理监听到新数据后自动触发做异常交易识别如果发现可疑项它会标记出来并通知报告代理优先处理这部分内容。这种模式的好处是每个代理职责单一便于独立测试和替换。我们后来换过一次分析代理的模型版本因为接口没变上层完全无感。2.3 Plugin 机制带来的扩展性Plugin 是我们这套方案里最灵活的部分。金融业务有个特点不同客户、不同产品线的需求差异极大你不可能把所有逻辑都写死在主流程里。我们的做法是把那些“可变部分”全部 plugin 化比如特定的数据源适配器、特定的合规规则集、特定的报告模板。主流程只负责编排具体能力通过 plugin 动态加载。这里有个设计决策值得说一下我们没有用动态链接库那种重型插件方案而是采用了基于配置的轻量级 plugin 注册机制。每个 plugin 就是一个独立的模块声明自己需要监听的上下文事件和能提供的服务接口由工作台在启动时统一注册。这样做的好处是插件之间完全解耦坏处是性能上有一点损耗但对于金融场景这种对吞吐要求不算极端的业务来说可维护性显然更重要。3. 核心模块拆解与关键实现细节3.1 代理注册与生命周期管理代理的注册是整个工作台的入口。我们设计了一个AgentRegistry模块每个代理在启动时向它注册自己的元信息包括代理 ID、能力描述、监听的上下文事件类型、以及一个健康检查端点。注册信息会持久化到数据库这样即使工作台重启也能恢复出完整的代理拓扑。生命周期管理这块我们踩过一个坑。最初的设计是代理一旦注册就常驻结果发现有些代理在空闲时也占着资源而且模型调用的计费是按 token 算的空闲代理虽然不调用模型但心跳检测本身也有开销。后来改成了按需激活模式代理默认处于休眠状态只有当它监听的事件被触发时才唤醒处理完任务后如果一段时间没有新事件就自动休眠。这个“一段时间”我们设的是 5 分钟实测下来在保证响应速度的同时资源占用降低了大概 40%。class AgentRegistry: def __init__(self, db_client): self.db db_client self.agents {} self.event_bus EventBus() def register(self, agent_meta): agent_id agent_meta[id] self.agents[agent_id] { meta: agent_meta, status: sleeping, last_active: None } self.db.save_agent(agent_meta) for event_type in agent_meta[listens]: self.event_bus.subscribe(event_type, agent_id) def activate(self, agent_id, context): agent self.agents.get(agent_id) if not agent: raise AgentNotFound(agent_id) agent[status] active agent[last_active] time.time() return self._dispatch(agent, context)上面这段是简化后的注册与激活逻辑。实际生产环境里还要加上权限校验和配额控制金融场景下不是每个代理都能访问所有数据的。3.2 上下文共享空间的设计与隔离上下文共享空间是代理协作的核心。我们把它设计成一个带版本控制的键值存储每个键对应一类业务数据比如transactions、risk_flags、report_draft。代理读写上下文都通过统一的接口每次写入都会生成一个新版本旧版本保留用于审计回溯。这里有个关键决策上下文要不要做隔离我们的答案是既要共享又要隔离。共享是为了协作隔离是为了安全。具体做法是按业务线划分命名空间比如ns:retail和ns:corporate是两个独立的上下文空间代理只能访问自己被授权的命名空间。跨命名空间的数据流转必须经过一个显式的“数据交换”步骤这个步骤会记录完整的流转日志。注意上下文里的数据在写入前一定要做脱敏处理。我们最初图省事直接把原始对账单文本塞进上下文结果发现模型在生成报告时会把完整的账号信息带出来。后来加了一层脱敏过滤器所有进入上下文的敏感字段都会被替换成占位符只有报告代理在最终输出时才会根据权限决定是否还原。3.3 模型调用的重试与降级策略金融场景对稳定性要求高但模型调用本身是有失败概率的。我们设计了一套分级重试策略第一次失败后等待 1 秒重试第二次失败后等待 3 秒重试第三次失败后不再重试而是触发降级逻辑。降级逻辑分两种一种是切换到备用模型比如从高配模型切到轻量模型另一种是返回一个“待人工处理”的标记把任务挂起。重试的等待时间不是拍脑袋定的。我们统计过模型调用的失败分布发现大部分瞬时失败在 1 秒内就能恢复所以第一次重试等待 1 秒是合理的。第二次等待 3 秒是因为如果 1 秒没恢复说明可能遇到了稍严重的网络抖动或服务端限流需要给系统更多缓冲时间。第三次直接降级是因为连续三次失败基本可以判定不是瞬时问题了继续重试只会浪费时间和配额。def call_model_with_retry(prompt, max_retries3): delays [1, 3] for attempt in range(max_retries): try: return model_client.invoke(prompt) except TransientError as e: if attempt len(delays): time.sleep(delays[attempt]) continue else: return fallback_handler(prompt) except PermanentError: raise3.4 Plugin 的加载与热更新Plugin 加载我们用的是“启动时全量加载 运行时按需热更新”的混合模式。启动时扫描 plugin 目录读取每个 plugin 的 manifest 文件校验依赖关系后注册到工作台。运行时如果检测到某个 plugin 有新版本会先加载新版本到隔离环境做一次冒烟测试通过后再切换流量。热更新这块有个细节值得展开。金融业务里有些 plugin 是有状态的比如维护着一个本地缓存。直接替换会导致缓存丢失进而引发性能抖动。我们的做法是在切换前先把旧 plugin 的状态导出注入到新 plugin 里然后再做流量切换。这个状态导出和注入的接口是 plugin 规范里强制要求的不实现就不允许热更新。Plugin 类型加载时机是否支持热更新状态处理方式数据源适配器启动时支持导出连接池配置新实例重建连接合规规则集启动时支持规则本身无状态直接替换报告模板按需支持模板无状态直接替换缓存插件启动时不支持需重启工作台4. 完整实操流程从环境准备到第一个代理上线4.1 环境准备与依赖安装我们这套工作台是跑在 Linux 环境下的推荐 Ubuntu 22.04 或更高版本。基础依赖包括 Python 3.10、PostgreSQL 14、Redis 7。Python 环境建议用 conda 或 venv 做隔离不要直接装在系统 Python 里不然后面升级依赖会非常痛苦。# 创建虚拟环境 python -m venv financial-services-env source financial-services-env/bin/activate # 安装核心依赖 pip install managed-agents-sdk1.2.0 pip install claude-client0.9.3 pip install cowork-runtime0.5.1 pip install psycopg2-binary redis pyyaml # 验证安装 python -c import managed_agents; print(managed_agents.__version__)这里要特别注意版本兼容性。Managed Agents SDK 和 Cowork Runtime 之间有版本对应关系我们实测下来 1.2.0 配 0.5.1 是稳定的如果混用其他版本可能会出现代理注册失败的问题。安装完成后建议跑一下官方提供的自检脚本确认基础环境没问题再往下走。4.2 工作台配置文件详解工作台的配置集中在一个 YAML 文件里我们叫它workbench.yaml。这个文件定义了数据库连接、Redis 连接、模型端点、代理注册表路径、plugin 目录等关键信息。下面是我们生产环境用的配置模板脱敏后分享出来。workbench: name: financial-services-workbench environment: production database: host: db.internal port: 5432 name: workbench user: workbench_app password: ${DB_PASSWORD} pool_size: 20 redis: host: cache.internal port: 6379 db: 0 password: ${REDIS_PASSWORD} model: primary_endpoint: https://api.internal/v1/messages primary_model: claude-sonnet fallback_model: claude-haiku timeout_seconds: 30 max_retries: 3 agents: registry_path: ./agents/registry heartbeat_interval: 30 idle_sleep_seconds: 300 plugins: directory: ./plugins hot_reload: true smoke_test_timeout: 10配置里有个地方容易忽略pool_size的设置。我们一开始设的是 10结果在高并发场景下经常出现连接等待。后来根据实际压测数据调整到 20连接等待时间从平均 200ms 降到了 30ms 以下。这个值不是越大越好太大反而会增加数据库负担建议根据实际并发量做压测后确定。4.3 第一个采集代理的实现采集代理的职责是从指定数据源拉取原始材料并写入上下文。我们以对账单采集为例展示一个最小可用的代理实现。这个代理监听statement.fetch.requested事件收到事件后调用数据源接口把结果写入ns:retail命名空间的raw_statements键。from managed_agents import BaseAgent, ContextClient from claude_client import ClaudeClient class StatementCollector(BaseAgent): def __init__(self, agent_id, config): super().__init__(agent_id, config) self.context ContextClient(namespacens:retail) self.claude ClaudeClient(modelclaude-haiku) def on_event(self, event): if event.type ! statement.fetch.requested: return source_url event.payload[source_url] raw_text self._fetch(source_url) parsed self._parse_with_model(raw_text) self.context.write(raw_statements, { source: source_url, content: parsed, fetched_at: time.time() }) self.emit(statement.fetch.completed, {source: source_url}) def _fetch(self, url): # 实际实现里这里会调用具体的数据源适配器 return data_source_client.get(url) def _parse_with_model(self, raw_text): prompt f请从以下对账单文本中提取交易记录输出 JSON 格式\n{raw_text} return self.claude.invoke(prompt)这个代理里有个设计点值得说解析这一步我们用了轻量模型而不是高配模型。原因是解析任务相对简单主要是格式转换和信息抽取轻量模型完全够用而且成本只有高配模型的十分之一左右。金融场景里这种“该省省该花花”的取舍很重要不是所有环节都需要最强模型。4.4 代理注册与上线验证代理写好后需要在注册表里登记。注册表是一个 JSON 文件放在agents/registry目录下每个代理一个文件。登记内容包括代理 ID、类路径、监听的事件类型、需要的权限等。{ id: statement-collector-01, class: agents.collectors.StatementCollector, listens: [statement.fetch.requested], emits: [statement.fetch.completed], permissions: { context_read: [ns:retail], context_write: [ns:retail], data_sources: [statement-api] }, resources: { memory_mb: 512, cpu_cores: 1 } }上线验证分三步走。第一步是单元测试在本地环境模拟事件触发确认代理能正确读写上下文。第二步是集成测试在工作台的测试环境里跑一遍完整流程确认代理之间的协作没问题。第三步是灰度发布先让代理处理 5% 的真实流量观察 24 小时确认没有异常后再逐步放大到全量。提示灰度发布期间一定要盯紧错误率和延迟两个指标。我们有一次灰度时发现错误率正常但 P99 延迟涨了三倍排查后发现是新代理的模型调用没有做超时控制个别慢请求拖累了整体。后来给所有模型调用都加上了硬超时问题才解决。5. 常见问题排查与避坑经验实录5.1 代理注册失败的那些原因代理注册失败是我们遇到频率最高的问题没有之一。表现是工作台启动时日志里报AgentNotFound或者RegistrationConflict。排查下来主要有三类原因。第一类是类路径写错了。注册表里的class字段必须是完整的 Python 路径而且工作台的PYTHONPATH要能覆盖到这个路径。我们有一次把代理放在了一个子目录里但PYTHONPATH没更新结果一直报找不到类。解决办法是在工作台启动脚本里显式设置PYTHONPATH把所有代理目录都加进去。第二类是事件类型冲突。两个代理监听了同一个事件而且都试图处理这会导致事件被重复消费。我们的规范是同一个事件类型只能有一个主处理器其他代理如果需要响应应该监听主处理器发出的事件。这个规范写在开发手册里但新人还是经常踩后来我们在注册阶段加了一个校验发现冲突直接拒绝注册并给出明确提示。第三类是权限配置不匹配。代理声明要读写的命名空间必须在工作台的权限白名单里否则注册会被拒绝。这个设计是为了防止代理越权访问数据。排查方法是检查注册表里的permissions字段和工作台配置里的allowed_namespaces是否一致。错误信息可能原因排查方法解决方案AgentNotFound类路径错误检查 PYTHONPATH 和 class 字段修正路径或补充 PYTHONPATHRegistrationConflict事件类型冲突检查 listens 字段是否重复改为监听下游事件PermissionDenied权限不匹配对比 permissions 和 allowed_namespaces补充白名单或调整权限声明DependencyMissing依赖未安装检查代理的 requirements安装缺失依赖5.2 上下文数据不一致的排查思路上下文数据不一致的表现是代理 A 写入了数据代理 B 读到的却是旧版本。这个问题在并发场景下尤其容易出现。我们的排查思路是沿着版本号追。每个上下文写入都会生成一个递增的版本号读取时可以指定版本号不指定则读最新。如果 B 读到了旧版本先看 B 读取时指定的版本号是多少再看 A 写入的版本号是多少。如果 B 指定的版本号小于 A 的写入版本号说明 B 读的是历史版本这是预期行为需要检查 B 的业务逻辑为什么用了旧版本。如果 B 没指定版本号却读到了旧版本那可能是缓存问题需要检查 Redis 里的缓存是否及时失效。我们后来在上下文客户端里加了一个read_after_write的选项写入后强制读一次最新版本做校验确认写入生效。这个选项会增加一点延迟但在关键路径上值得开。5.3 模型输出格式不稳定的应对金融场景对输出格式要求严格但模型有时候会“自由发挥”比如要求输出 JSON 却输出了带解释文字的 JSON。我们的应对策略是三层防护。第一层是 prompt 里明确格式要求并且给出示例。示例很重要模型看到示例后遵循格式的概率会大幅提升。第二层是输出后做格式校验如果不符合预期触发一次“格式修正”调用把原始输出和格式要求一起发给模型让它重新输出。第三层是如果修正后仍然不符合就降级到人工处理队列不再自动重试。def ensure_json_output(raw_output, schema): try: parsed json.loads(raw_output) validate_schema(parsed, schema) return parsed except (json.JSONDecodeError, SchemaError): corrected model_client.invoke( f请将以下内容修正为符合 {schema} 的 JSON 格式只输出 JSON\n{raw_output} ) try: return json.loads(corrected) except json.JSONDecodeError: raise ManualInterventionRequired(raw_output)这套机制跑下来自动处理率大概在 95% 左右剩下 5% 需要人工介入。对于金融场景来说这个比例是可以接受的毕竟有些复杂文档确实需要人的判断。5.4 性能瓶颈的定位与优化性能问题我们遇到过两次比较典型的。一次是上下文读写成为瓶颈原因是所有代理都往同一个 Redis 实例读写QPS 上来后延迟飙升。解决办法是按命名空间做分片不同业务线的上下文走不同的 Redis 实例压力分散后延迟恢复正常。另一次是模型调用排队。我们的模型端点有并发限制当同时活跃的代理超过限制时后来的请求会排队。排队本身没问题但有些代理的超时设置太短排队还没结束就超时了导致任务失败。后来我们统一调整了超时策略排队等待时间不计入超时只有实际调用时间才计入。这个调整需要在客户端层面做把排队和调用两个阶段分开计时。注意性能优化一定要有数据支撑不要凭感觉调参。我们每次调整前都会先跑一轮基准测试记录调整前的 QPS、P50、P95、P99 延迟调整后再跑一轮对比。没有数据对比的优化都是耍流氓。6. 安全合规与审计日志的落地实践6.1 数据脱敏的分层设计金融数据脱敏不是一刀切我们设计了三层脱敏策略。第一层是入口脱敏所有进入工作台的原始数据在写入上下文前敏感字段账号、身份证号、手机号都会被替换成占位符。第二层是上下文脱敏代理之间传递数据时根据接收方的权限决定是否还原敏感字段。第三层是出口脱敏最终生成的报告在输出前做最后一次检查确保没有敏感信息泄露。这三层脱敏的规则是集中管理的放在一个独立的规则文件里支持热更新。规则文件里定义了每种敏感字段的识别模式和替换策略。比如账号的识别模式是\d{16,19}替换策略是保留前四位和后四位中间用星号代替。6.2 审计日志的字段设计与存储审计日志我们记录了非常细的粒度每一条日志包含时间戳、代理 ID、操作类型、上下文命名空间、读写的数据键、数据版本号、模型调用 ID如果有、耗时、结果状态。这些字段看起来多但排查问题时一个都不能少。存储上我们用了冷热分离。最近 7 天的日志存在 PostgreSQL 里支持快速查询。超过 7 天的日志归档到对象存储需要的时候再拉回来。归档格式是 Parquet压缩比高查询也方便。日志保留期限是 3 年这是金融行业的普遍要求。CREATE TABLE audit_log ( id BIGSERIAL PRIMARY KEY, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), agent_id VARCHAR(64) NOT NULL, operation VARCHAR(32) NOT NULL, namespace VARCHAR(64), data_key VARCHAR(128), data_version INTEGER, model_call_id VARCHAR(64), duration_ms INTEGER, status VARCHAR(16) NOT NULL ); CREATE INDEX idx_audit_agent_time ON audit_log (agent_id, timestamp DESC); CREATE INDEX idx_audit_namespace ON audit_log (namespace, timestamp DESC);索引的设计也有讲究。我们最常用的查询是“某个代理在某个时间段内的所有操作”和“某个命名空间在某个时间段内的所有操作”所以建了这两个组合索引。其他查询场景相对少走全表扫描也能接受。6.3 权限模型与最小权限原则权限模型我们用的是基于角色的访问控制角色定义在配置里代理注册时声明自己需要的角色。角色和权限的映射关系是集中管理的修改后需要重启工作台生效。最小权限原则说起来简单做起来难。我们最初给采集代理开了很大的权限因为它要访问多个数据源。后来做安全审计时发现采集代理其实只需要读权限不需要写权限写操作是分析代理做的。调整后采集代理的权限收窄了安全风险也降低了。这个案例告诉我们权限设计一定要跟着实际数据流走不要凭直觉给权限。7. 扩展方向与个人实操体会这套工作台跑了大半年整体稳定性还不错但有几个方向我们还在持续打磨。一个是代理的自动扩缩容目前还是手动配置实例数理想状态是根据事件队列的长度自动调整。另一个是跨工作台的代理协作现在代理只能在同一个工作台内通信未来希望能支持跨工作台的联邦式协作。还有一个是模型路由的智能化现在主备模型的切换是静态配置的未来想根据任务类型和实时负载动态选择模型。我个人在实际操作中的体会是金融场景做智能代理最大的挑战不是技术本身而是如何在灵活性和可控性之间找到平衡。模型能力很强但你不能完全放手让它跑必须有一层层的校验和兜底。我们内部有个说法叫“信任但验证”每个代理的输出都要经过至少一道校验才能进入下一环节。这个做法会增加一些延迟和成本但在金融场景里正确性永远比速度重要。最后分享一个小技巧代理的 prompt 一定要版本化管理和代码一样走 Git。我们有一次排查一个输出格式问题查了半天发现是有人直接在生产环境改了 prompt 但没记录导致行为和测试环境不一致。后来我们把所有 prompt 都抽出来放在独立的文件里每次修改都要走代码评审这个问题就再也没出现过。