实战进阶

能力的三层拆分

解析生产级 Agent 架构的三层设计哲学:提示策略层、确定性工具层与受限沙箱执行层的职责边界与解耦方案。

在传统的单体 AI 脚本中,开发者往往直接在大模型工具函数(Tool)内部编写具体的操作系统命令或网络请求。例如在 execute_sql 工具中直接引入 pg 客户端并执行查询。这种设计一旦面对多环境切换(如云端 PostgreSQL 与本地 SQLite)、Mock 测试或安全沙箱隔离时,就会导致严重的强耦合问题。

DeepSeek Harness 确立了业界领先的 “三层拆分架构(Three-Role Capability Design)”:通过将智能体的每一个能力解耦为 Seam(抽象缝隙)Provider(底层实现)Tool(模型工具) 三个独立的 npm 包,实现了极致的可插拔性与工程健壮度。

本文将以一个 “智能体数据库操作能力(Database Capability)” 为例,深度剖析三层拆分设计模式的实战实现。


架构透视:三层拆分的核心分工与拓扑

┌─────────────────────────────────────────────────────────────┐
│                   DeepSeek 三层拆分架构拓扑                 │
├─────────────────────────────────────────────────────────────┤
│  第 3 层:Tool (面向大模型)                                  │
│  • 包名:dsh-tool-sql                                       │
│  • 职责:导出 defineTool、参数 Schema 校验、返回结果渲染     │
│  • 依赖:只依赖 Seam 抽象,绝对不引入具体数据库驱动          │
├──────────────────────────────▲──────────────────────────────┤
│                              │ 消费 ctx.dbDriver 服务        │
├──────────────────────────────┴──────────────────────────────┤
│  第 1 层:Seam (抽象扩展缝隙)                                │
│  • 包名:dsh-database-seam                                  │
│  • 职责:定义 IDatabaseDriver 接口与 declare module 服务契约│
│  • 依赖:纯 TypeScript 类型与抽象类,零外部依赖              │
├──────────────────────────────▲──────────────────────────────┤
│                              │ 实现并挂载 ctx.dbDriver       │
├──────────────────────────────┴──────────────────────────────┤
│  第 2 层:Provider (底层物理实现)                            │
│  • 包名:dsh-database-postgres (或 dsh-database-sqlite)      │
│  • 职责:引入底层驱动,实现真实 SQL 执行与连接池生命周期管理│
└─────────────────────────────────────────────────────────────┘

步骤一:编写 Seam 抽象层 (dsh-database-seam)

Seam 层是连接 Tool 与 Provider 的唯一契约。它绝不包含具体实现代码,只包含接口定义与微内核服务的类型挂载:

typescript
// packages/dsh-database-seam/src/index.ts
import { Service, type Context } from '@deepseek-ai/cordis';

// 1. 核心业务接口契约
export interface QueryResult {
    columns: string[];
    rows: Record<string, any>[];
    rowCount: number;
}

export interface IDatabaseDriver {
    query(sql: string, params?: any[]): Promise<QueryResult>;
    ping(): Promise<boolean>;
}

// 2. TypeScript 模块声明合并
declare module '@deepseek-ai/cordis' {
    interface Context {
        dbDriver: IDatabaseDriver;
    }
}

// 3. 导出抽象基类
export abstract class DatabaseDriverSeam extends Service implements IDatabaseDriver {
    constructor(ctx: Context) {
        super(ctx, 'dbDriver');
    }

    abstract query(sql: string, params?: any[]): Promise<QueryResult>;
    abstract ping(): Promise<boolean>;
}

步骤二:编写 Provider 实现层 (dsh-database-postgres)

Provider 层负责实现 Seam 中定义的接口,并引入底层的重量级依赖(如 pg 客户端):

typescript
// packages/dsh-database-postgres/src/index.ts
import type { Context } from '@deepseek-ai/cordis';
import { DatabaseDriverSeam, type QueryResult } from 'dsh-database-seam';

export const name = 'postgres-database-provider';

class PostgresDriverImpl extends DatabaseDriverSeam {
    // 模拟真实数据库连接池
    async query(sql: string, params: any[] = []): Promise<QueryResult> {
        console.log(`[Postgres] 执行 SQL 查询: ${sql}`);
        return {
            columns: ['id', 'user_name', 'status'],
            rows: [
                { id: 1, user_name: 'Alice', status: 'active' },
                { id: 2, user_name: 'Bob', status: 'pending' }
            ],
            rowCount: 2
        };
    }

    async ping(): Promise<boolean> {
        return true;
    }
}

export function apply(ctx: Context) {
    // 挂载 Provider 实例
    ctx.plugin(PostgresDriverImpl);
}

步骤三:编写 Tool 工具层 (dsh-tool-sql)

Tool 层面向大模型。它通过 inject: ['dbDriver'] 注入抽象服务,并通过 output.render 将查询结果格式化为 Markdown 表格:

typescript
// packages/dsh-tool-sql/src/index.ts
import type { Context } from '@deepseek-ai/cordis';
import { defineTool } from '@deepseek-ai/dsh-tools';
import 'dsh-database-seam'; // 引入 Seam 提供的类型支持

export const name = 'sql-query-tool';
export const inject = ['tools', 'dbDriver'];

export function apply(ctx: Context) {
    ctx.tools.register(defineTool({
        name: 'execute_sql_query',
        description: '执行只读 SQL 查询,并返回格式化的数据行。用于检索数据库元数据或业务指标。',
        parameters: {
            sql: {
                type: 'string',
                required: true,
                description: '待执行的标准 SELECT SQL 语句'
            }
        },
        output: {
            schema: { type: 'object' },
            render: (_args, result) => [
                {
                    type: 'text',
                    text: `[SQL 执行结果 (共 ${result.rowCount} 条)]
` +
                          `| ${result.columns.join(' | ')} |
` +
                          `| ${result.columns.map(() => '---').join(' | ')} |
` +
                          result.rows.map((r: any) => `| ${Object.values(r).join(' | ')} |`).join('
')
                }
            ]
        },
        async execute(args) {
            // 直接调用 ctx.dbDriver,完全不需要知道底层是 Postgres 还是 SQLite
            return await ctx.dbDriver.query(args.sql);
        }
    }));
}

三层拆分架构的三大核心工程收益

┌─────────────────────────────────────────────────────────────┐
│                 三层拆分核心工程价值                        │
├─────────────────────┬───────────────────────────────────────┤
│ 1. Provider 自由置换│ 本地测试使用 SQLite Provider,生产环境 │
│                     │ 无缝切为 Postgres Provider,Tool 零改动│
├─────────────────────┼───────────────────────────────────────┤
│ 2. 依赖彻底解耦     │ Tool 客户端包只有几 KB,无需安装重量级│
│                     │ C++ 原生驱动二进制包                   │
├─────────────────────┼───────────────────────────────────────┤
│ 3. 独立版本迭代     │ 底层驱动版本升级不影响大模型的 Tool    │
│                     │ 提示词与 Schema 契约                  │
└─────────────────────┴───────────────────────────────────────┘

常见问题解答 (FAQ)

Q1: 为什么不能直接将 Provider 与 Tool 写在同一个 npm 包中?

解答:如果将两者揉合,当需要在 WebAssembly、Docker 容器或轻量端侧切换驱动时,整个 Tool 也必须重写;同时还会将繁重的底层驱动依赖强行引入到不需要该驱动的环境中。

Q2: 如果当前运行环境中只挂载了 Tool,忘记挂载 Provider 会发生什么?

解答:Tool 声明了 inject = ['dbDriver']。由于缺少提供 dbDriver 服务的 Provider,Tool 会自动停留在 PENDING 挂起状态,绝不会在运行时因访问未定义的属性而抛出崩溃异常。

Q3: 官方仓库中的 Bash 工具是如何遵循三层拆分的?

解答:官方将 Bash 能力拆分为:dsh-shell(Seam 接口契约)、dsh-bash-local(Provider 本地进程实现)、dsh-tool-bash(Tool 大模型交互入口)。这也是 Harness 体系最标准的基准范式。

Q4: 如何为同一个 Seam 同时注册多个不同名称的 Provider 实例?

解答:可以在 Seam 中设计注册中心模式(如 ctx.dbManager.registerDriver('prod', driver)),并在 cordis.yml 中配置路由选择策略。