Skip to content

DeepSeek Harness 系列(四):接入真实业务的工具化方法

Agent 进入企业系统时,最危险的做法是把数据库、HTTP 接口和内部文档一股脑交给模型,让模型“自己判断”。这不是智能,是把业务边界挖穿。

DeepSeek Harness 更适合的接入方式是:把业务能力包装成插件和工具,让模型只能通过受约束的能力完成任务。工具参数要清楚,输出要结构化,写操作要经过审批,结果要进入会话日志,审计要能复盘。

本文用一个经过匿名化处理的真实业务模式说明这件事:业务系统会创建 AI 审核任务,正常路径依赖外部 AI 回调写入结果;当外部结果已经产生但回调没有触达业务系统时,业务系统内任务可能长期停留在运行中状态。下面不会展开某个内部系统的实现细节,而是抽出适合迁移到 Harness 的架构模式。

业务背景

这个业务的核心问题是:业务会签系统会创建 AI 审核任务,正常情况下由外部 AI 回调写入结果。如果 AI 侧已经有结果,但推送或回调没有触达业务系统,业务系统内任务就可能长期停留在 RUNNING

现有业务实现提供了两类能力。

能力已有业务入口
运行中任务补偿业务系统提供受控的补偿调度接口
异常巡检查询查询失败任务和超时运行中的当前 AI 任务

在这个匿名化模式里,补偿接口会调用业务服务层的“补偿运行中任务”能力。服务层会扫描状态为 RUNNING 的当前 AI 任务,主动查询外部 AI 状态,并在任务进入终态后复用正常回调路径保存任务结果与规则结果。只有任务仍是当前有效任务,并且成功结果完整,系统才会触发质量字段覆盖、风险通知等后续逻辑。

这个业务链路很适合拿来讲 Agent 接入,因为它同时包含只读查询、写操作、幂等、当前/历史任务判断、外部 AI 状态、业务尾处理和运维可观测性。

不要让 Agent 直接碰表

如果把这个能力接给 Agent,最直接但不推荐的做法是给它一个 SQL 工具,然后提示它:

text
帮我查一下 AI 失败和超时任务,如果能补偿就调用接口。

这个方式的问题很明显:

  • 模型可能改写 SQL 条件,误把历史任务当当前异常。
  • 模型可能绕开正常回调保存路径,直接更新任务表或质量字段。
  • 模型可能重复触发补偿,造成通知或状态处理异常。
  • 审计日志很难回答“这次补偿为什么执行、查到了什么、改了什么”。

更好的做法是把业务系统已有边界保留下来。

Agent 只接触两个工具,不接触表结构细节。工具背后仍然走业务已有接口和服务层。这样,模型可以参与排查和决策,但业务规则仍由业务系统执行。

把真实业务拆成 Harness 能力

这个场景至少可以拆成三个工具。

工具类型作用风险等级
query_ai_task_anomalies只读查询失败和超时的当前 AI 任务。
preview_ai_task_compensation只读预览本次最多会处理多少运行中任务。
compensate_ai_tasks写操作调用补偿接口,触发业务服务层。

低风险查询可以直接执行。高风险补偿必须走审批、限流和审计,并且最好要求明确的 batchSize、业务说明和操作者意图。

这个流程把“智能判断”和“业务动作”分开了。Agent 可以整理异常、解释风险、建议下一步;真正改变状态的动作,必须穿过工具流水线。

工具定义示意

下面是一个 Harness 工具插件的示意写法。它不是可以直接复制运行的完整包,而是展示边界应该怎么设计。

ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'ai-task-risk-control-tools'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'query_ai_task_anomalies',
    description: 'Query AI audit tasks that are failed or running longer than the configured threshold.',
    parameters: {
      limit: {
        type: 'integer',
        description: 'Maximum number of anomaly rows to return.',
      },
    },
    output: {
      schema: {
        type: 'object',
        additionalProperties: false,
        properties: {
          total: { type: 'integer' },
          rows: {
            type: 'array',
            items: {
              type: 'object',
              additionalProperties: false,
              properties: {
                businessObjectId: { type: 'string' },
                aiTaskId: { type: 'string' },
                ownerName: { type: 'string' },
                anomalyType: { type: 'string' },
                occurredAt: { type: 'string' },
              },
            },
          },
        },
      },
      render: (_args, value) => [{
        type: 'text',
        text: `Found ${value.total} AI task anomaly record(s).`,
      }],
    },
    async execute(args, exec) {
      return queryAiTaskAnomalies({
        limit: args.limit ?? 20,
        signal: exec.signal,
      })
    },
  }))
}

这个工具有几个设计点:

  • 参数是受控的,只有 limit,不暴露任意 SQL。
  • 输出是结构化 JSON,模型不用从自然语言里解析 id。
  • execute() 接收 exec.signal,可以响应取消。
  • render() 只负责模型可见摘要,完整结构化值仍给程序化消费方使用。

写操作工具也类似,但要更保守。

ts
ctx.tools.register(defineTool({
  name: 'compensate_ai_tasks',
  description: 'Run the approved AI task compensation endpoint.',
  parameters: {
    batchSize: {
      type: 'integer',
      required: true,
      description: 'Number of RUNNING tasks to compensate. Must be between 1 and 500.',
    },
    reason: {
      type: 'string',
      required: true,
      description: 'Operator-visible reason for this compensation run.',
    },
  },
  output: {
    schema: {
      type: 'object',
      additionalProperties: false,
      properties: {
        success: { type: 'boolean' },
        message: { type: 'string' },
        totalCount: { type: 'integer' },
        succeededStatusCount: { type: 'integer' },
        failedStatusCount: { type: 'integer' },
        updatedCount: { type: 'integer' },
        skippedCount: { type: 'integer' },
        errorCount: { type: 'integer' },
      },
    },
    render: (_args, value) => [{
      type: 'text',
      text: value.message,
    }],
  },
  async execute(args, exec) {
    if (args.batchSize < 1 || args.batchSize > 500) {
      throw new Error('batchSize must be between 1 and 500.')
    }

    return callAiTaskCompensationEndpoint({
      batchSize: args.batchSize,
      reason: args.reason,
      signal: exec.signal,
    })
  },
}))

注意这里仍然没有让工具直接更新表。它调用的是现有业务接口。原因很简单:现有业务接口已经知道怎么复用正常回调保存路径,怎么判断当前任务,怎么处理终态和失败,怎么保持和正常回调路径一致。

用工具流水线承载审批

补偿工具属于写操作,不能只靠提示词提醒模型谨慎。应该把审批做成工具策略。

策略插件应该看工具名、参数、会话来源和当前权限模式,再决定是否 allowdenyask。这样审批不是写在某个业务工具里的孤岛,而是所有高风险工具共享的制度。

补偿类工具还建议增加几条守卫:

守卫原因
batchSize 上限防止一次补偿影响过大。业务接口和工具侧都应设置最大处理量。
必填 reason让会话日志里留下操作者意图,便于审计。
禁止并发补偿业务服务层应保持互斥,工具侧仍应避免模型连续发起多次。
只允许指定环境避免测试会话误打生产接口。
结果脱敏不把敏感用户、合同正文或内部链接过度暴露给模型。

让 Agent 输出有用结果

业务工具接上后,不要让 Agent 只返回“调用成功”。更有用的输出应该包括:

  • 发现了多少失败和超时任务。
  • 哪些是当前任务,哪些只是历史留痕。
  • 本次补偿读取了多少运行中任务。
  • AI 侧返回了多少成功终态和失败终态。
  • 更新了多少任务,跳过了多少任务,异常多少条。
  • 哪些残余风险需要人工或测试库验证。

这正好对应补偿接口应该返回的统计维度:总扫描数、成功终态数、失败终态数、已更新数、已跳过数、异常数,以及是否因为已有补偿运行而跳过。

这类输出能真正帮人工作。Agent 不只是执行器,它把业务状态、工具结果和残余风险组织成可读判断。

真实业务里的边界设计

把这个案例迁移到 Harness 时,建议保留以下边界。

边界保留方式
当前任务判断仍由业务系统根据当前 AI 任务标识判断。
任务状态枚举工具只接受业务已有的 RUNNINGSUCCEEDEDFAILED
成功结果完整性只有整体结论和规则结果完整时才允许后续逻辑。
失败处理FAILED 只保存失败状态和原因,不触发质量字段覆盖。
异常巡检SQL 或只读 API 只查询当前任务,排除已归档、作废、终止对象。
幂等与互斥业务服务层保持互斥,工具侧避免连续高风险调用。
审计工具参数、审批原因和结果统计进入 Harness 会话日志。

这些边界的共同点是:Agent 参与的是编排和解释,不取代业务系统的权威判断。

一个更完整的接入架构

如果要在企业内部试点,可以按下面的架构推进。

这里最好加一层内部 API 网关,而不是让 Harness 插件直接连生产库。网关可以承担认证、环境隔离、频率限制和审计编号;Harness 负责 Agent 侧的工具参数、审批和会话日志。

接入 Checklist

落地前可以用这份清单自检。

检查项通过标准
工具是否结构化参数和输出都有 schema,不依赖自然语言解析 id。
是否避免任意 SQLAgent 不能直接拼接业务 SQL。
写操作是否审批高风险工具经过 tools/pre-execute 审批或拒绝。
是否复用业务入口写状态仍走现有服务层/API,不绕过业务规则。
是否有幂等保护业务侧和工具侧都能处理重复调用。
是否记录意图写操作要求 reason,并进入会话日志。
是否能回放工具结果能从 tool/result 解释本次发生了什么。
是否脱敏合同正文、用户信息、内部链接按最小必要输出。
是否区分环境测试、预发、生产 endpoint 不靠模型判断。

小结

DeepSeek Harness 接企业业务,不应该从“让模型拥有更多权限”开始,而应该从“把业务能力变成更清晰的工具边界”开始。

这个匿名化案例说明了一条比较稳的路线:业务系统继续拥有任务状态、当前任务判断、结果保存和尾处理;Harness 负责把异常巡检、补偿执行、审批审计和结果解释组合成 Agent 工作流。这样做不会削弱业务规则,反而能让 Agent 的每一步都落在可追踪、可审批、可复盘的工程边界里。

系列导航:上一篇:从源码运行 Web UI