← 返回文章列表

DeepSeek Harness 业务接入实践:构建可调用业务服务的 Agent 后端

把订单查询封装为工具,拆解 Session、鉴权、审批、超时、错误处理和 API 网关等业务接入边界。

开发环境中显示的程序代码

把 DeepSeek Harness 接进业务系统时,最容易走偏的一步是把它当成聊天接口:前端发一段文本,后端把文本转给模型,再把回答原样返回。这样虽然能聊,但没有用上 Harness 的核心价值。真正的接入点是 Session、工具和执行策略。

本文仍以 DeepSeek Harness commit 47f943859bef60e4160492346772ded9b24f765a 为准。目标很小:让 Agent 根据订单号查询一个已有订单服务,不让模型直接接触数据库。

先划清系统职责

一个比较稳妥的结构如下:

Web / App
   |
业务 API 网关:登录、租户、限流、审计
   |
DeepSeek Harness:Session、Agent Loop、模型、工具策略
   |
order_query 工具
   |
现有订单服务:权限、业务规则、数据库事务

DeepSeek Harness 负责决定何时调用工具、维护对话过程和记录工具结果。订单服务仍然负责“这个用户能不能看这张订单”。不要把权限判断只写进提示词,也不要让工具拼 SQL。

极简订单服务

为了把注意力放在接入链路上,可以先用 Node.js 内置 HTTP 服务模拟现有业务:

import { createServer } from 'node:http'

const orders = {
  A100: { id: 'A100', status: 'paid', amount: 199 },
}

createServer((req, res) => {
  const id = req.url?.split('/').pop()
  const order = id ? orders[id] : undefined
  res.setHeader('content-type', 'application/json')
  res.writeHead(order ? 200 : 404)
  res.end(JSON.stringify(order ?? { error: 'order_not_found' }))
}).listen(4010, '127.0.0.1')

正式服务通常已经有鉴权、数据库和领域错误。这个 Mock 只保留 200 与 404 两条路径,足够验证 Agent 会不会正确调用工具。

把业务接口注册成工具

DeepSeek Harness 使用 defineTool 定义模型可见参数、返回值和执行函数。下面省略了 package.json 和配置 schema,只保留核心代码:

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

export const name = 'order-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'order_query',
    description: 'Query one order that the current user is allowed to view.',
    parameters: {
      orderId: {
        type: 'string',
        required: true,
        description: 'Order id, for example A100.',
      },
    },
    output: {
      schema: {
        type: 'object',
        properties: {
          id: { type: 'string' },
          status: { type: 'string' },
          amount: { type: 'number' },
        },
        required: ['id', 'status', 'amount'],
        additionalProperties: false,
      },
      render: (_args, value) => [{
        type: 'text',
        text: `Order ${value.id}: ${value.status}, amount ${value.amount}`,
      }],
    },
    async execute({ orderId }, exec) {
      const response = await fetch(
        `http://127.0.0.1:4010/orders/${encodeURIComponent(orderId)}`,
        { signal: exec.signal },
      )
      if (!response.ok) {
        throw new Error(`order service returned ${response.status}`)
      }
      return await response.json()
    },
  }))
}

参数由工具 schema 校验,模型生成了错误类型时不会进入 execute。执行异常会被工具运行时转换成错误结果,Agent 可以据此向用户说明订单不存在或服务暂时不可用。

这个 Demo 还缺一个生产环境必须有的字段:调用者身份。实际工具不能相信模型传入 userId,应从当前 Agent/Session 绑定的可信身份中取得用户和租户,再签发内部服务凭证。模型只负责提供订单号。

Session 不能直接等同于登录用户

同一个用户可以有多个会话,一个客服坐席也可能在不同租户间切换。比较清晰的映射方式是:

tenant_id + user_id + conversation_id -> DSH session_id

网关在创建或恢复 Session 时写入身份上下文,工具执行时读取这份可信上下文。日志和持久化目录也应带租户隔离,不能只依赖一个全局 Session ID。

DeepSeek Harness 的 Session Event 是追加日志。用户消息、模型消息、工具调用和结果都从日志派生,这对审计很有用,但也意味着日志中可能包含订单状态等业务数据。保存期限、脱敏和删除流程要按业务数据处理,不能因为它叫“Agent 日志”就绕开现有规范。

外层接口怎么做

DeepSeek Harness 提供 Web、headless、ACP 和 JSON-RPC 等组合,但它不是一个承诺兼容 OpenAI Chat Completions 的稳定业务接口。项目仍在快速迭代,直接让移动端绑定内部协议会把版本变化扩散到所有客户端。

更实用的做法是在自己的服务里保留一个窄接口,例如:

POST /api/assistant/messages
Authorization: Bearer <user-token>

{
  "conversationId": "c-42",
  "message": "查一下订单 A100"
}

网关完成四件事:验证用户、解析租户、找到 DSH Session、把流式事件转换成业务前端需要的格式。客户端不需要知道 Profile、Cordis 或工具事件的内部结构。

接入时容易忽略的几件事

读取订单和取消订单不是同一类工具。读操作可以按策略自动执行,写操作应要求明确确认,并让订单服务再次校验权限与状态。工具描述不是安全策略,真正的拒绝要发生在执行路径上。

网络调用必须响应 AbortSignal,并在工具执行层设置超时。重试只适合幂等读请求;创建、退款、取消等操作需要业务幂等键,不能让 Agent Loop 自己猜测是否重试。

模型输出、工具参数和业务返回值应该分别记录。日志中不要出现服务密钥,错误消息也不要把内部堆栈直接交给模型。模型需要的是可行动的信息,例如“订单服务超时”,而不是数据库地址。

这套接入方式的意义

DeepSeek Harness 不是替代业务后端,而是在模型与业务能力之间提供可组合的运行时。业务系统继续掌握身份、规则和事务;Harness 负责推理循环、工具调度、Session 与执行策略。两边边界清楚后,换模型、加工具或替换界面都不会要求重写订单服务。

参考源码

  • DeepSeek Harness 国内镜像
  • JSON-RPC 示例:examples/jsonrpc-agent
  • 工具开发参考:docs/cookbook/adding-a-tool.md
  • 工具执行管线:docs/tool-execution-pipeline.md