DeepSeek Harness Hands-On: Eine austauschbare Fähigkeit schreiben

Das vorige Kapitel hat das Konzept der drei Rollen vorgestellt; in diesem werden wir es nach der offiziellen Anleitung in Code umsetzen.

Wir werden eine Fähigkeit namens myCap implementieren: Sie nimmt einen Text als Eingabe und gibt alles in Großbuchstaben aus. Sie ist klein genug, um auf einen Blick verstanden zu werden, deckt aber dennoch vollständig die drei Pakete ab: Definition, Provider und Consumer.

Drei-Schritt: Service Definition (abstrakte Klasse + Typ) → Service Provider (konkrete Unterklasse) → Consumer (defineTool).

Schließlich laden Sie den Provider und den Consumer zusammen in cordis.yml.


Schritt 1: Schreiben Sie die Service-Definition

Die Service-Definition deklariert die Fähigkeit selbst: wie der Dienst heißt, wie er aufgerufen wird und welche Typen für Anfrage und Ergebnis gelten.

Sie enthält keine Implementierungslogik, nur eine abstrakte Methode und zwei Schnittstellen.

Die abstrakte Klasse MyCapService erbt von Service und registriert sich durchsuper(ctx, 'myCap')als benannter Dienst.

Das Declaration Merging von declare module gibt ctx.myCap seine Typen; diese Technik wurde in Teil 14 behandelt.

Beispiel

// Dateipfad: packages/my-cap/my-cap/src/index.ts
import { Service, type Context } from '@deepseek-ai/cordis'

// Declaration Merging: sorgen Sie dafür, dass ctx.myCap Typ-Hinweise in TypeScript hat
declare module '@deepseek-ai/cordis' {
  interface Context {
    myCap: MyCapService
  }
}

// Abstrakte Klasse: Das Definition-Paket deklariert nur Verträge, keine Implementierungen.
export abstract class MyCapService extends Service {
  constructor(ctx: Context) {
    super(ctx, 'myCap') // Als benannten Dienst ctx.myCap registrieren
  }

  /** Führt die Capability aus. */
  abstract execute(request: MyCapRequest): Promise<MyCapResult>
}

// Anfragetyp: Der Aufrufer muss Eingaben bereitstellen
export interface MyCapRequest {
  input: string
}

// Ergebnistyp: Capability gibt Ausgabe zurück
export interface MyCapResult {
  output: string
}

abstract execute ist die einzige abstrakte Methode, und der Provider muss sie implementieren.

MyCapRequest hat ein Feld input, und MyCapResult hat ein Feld output.

Die Request- und Result-Typen gehören zum Definition-Paket, und sowohl Provider als auch Consumer importieren sie daraus.


Schritt 2: Den Service Provider schreiben

Der Service Provider erbt von der abstrakten Klasse und implementiert das tatsächliche Verhalten.

MyCapLocal wandelt input in Großbuchstaben um und gibt es zurück.

export const name deklariert den Plugin-Namen; in apply die Service-Implementierung über ctx.plugin(MyCapLocal) registrieren.

Beispiel

// Dateipfad: packages/my-cap/my-cap-local/src/index.ts
import type { Context } from '@deepseek-ai/cordis'
import { MyCapService, type MyCapRequest, type MyCapResult } from '@deepseek-ai/dsh-my-cap'

// Implementierungsklasse: hängt nur vom Definition-Paket ab
class MyCapLocal extends MyCapService {
  async execute(request: MyCapRequest): Promise<MyCapResult> {
    // Verhalten des lokalen Providers.
    return { output: request.input.toUpperCase() }
  }
}

export const name = 'my-cap-local'

export function apply(ctx: Context) {
  // Implementierungsklasse als Plugin laden und sie als tatsächlichen Dienst für ctx.myCap registrieren
  ctx.plugin(MyCapLocal)
}

Der Provider hängt nur von den abstrakten Klassen und Typen im Definition-Paket ab.

Er kümmert sich nicht darum, wie das Modell ihn aufruft, noch darum, wie das Tool aussieht.

Wenn Sie später zu einer anderen Implementierung wechseln möchten, zum Beispiel einer, die auf einem Remote-Server läuft, müssen Sie nur eine weitere Provider-Unterklasse schreiben.


Schritt 3: Die Consumer-Seite schreiben

Der Consumer kapselt die Fähigkeit in ein Tool, das das Modell aufrufen kann.

Die inject-Deklaration hängt von tools und myCap ab und stellt sicher, dass apply erst ausgeführt wird, nachdem beide Dienste bereit sind.

defineTool deklariert das Tool-Schema; sobald das Modell das Tool my_cap sieht, übergibt es Parameter gemäß parameters.

Rufe in execute ctx.myCap.execute auf und gib das Ergebnis als Tool-Ausgabe zurück.

Beispiel

// Dateipfad: packages/my-cap/tool-my-cap/src/index.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'tool-my-cap'
// Abhängigkeitsdeklaration: tools stellt den Registrierungseintrag bereit, myCap stellt die Dienstimplementierung bereit.
export const inject = ['tools', 'myCap']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'my_cap', // Tool-Name für das Modell
    description: 'Führe meine Fähigkeit aus.',
    // Parameterschema: Das Modell generiert Parameter entsprechend
    parameters: {
      input: { type: 'string', required: true },
    },
    // Ausgabeschema und Rendering-Methode
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    // Tatsächliche Ausführung: Aufruf des Capability-Service
    async execute(args) {
      const result = await ctx.myCap.execute({ input: args.input })
      return result.output
    },
  }))
}

Das Modell kennt ctx.myCap nicht direkt; es kennt nur das Tool my_cap.

Der Consumer ist die Brücke zwischen den beiden und außerdem dafür verantwortlich, das Ergebnis in für das Modell sichtbaren Text zu rendern.


Die Drei-Rollen-Struktur und der Datenfluss von myCap

Wenn die drei Pakete zusammengesetzt sind, fließen die Daten vom Modell durch Consumer und Definition und landen in Provider.

myCap 三角色结构与数据流图

Das Modell ruft zuerst das Tool my_cap auf; dieser Schritt findet nur auf der Consumer-Ebene statt.

Consumer übergibt parameterPackage als MyCapRequest und reicht es an ctx.myCap.execute(request) weiter.

Definition delegiert den Aufruf an die aktuell geladene Provider-Implementierung.

Nachdem der Provider das Ergebnis berechnet hat, gibt er MyCapResult entlang des ursprünglichen Pfads an den Consumer zurück.

Der Consumer rendert die Ausgabe in Text und übergibt sie dem Modell als Tool-Ergebnis.

Über die gesamte Kette hinweg bemerkt weder der Consumer noch das Modell, wenn der Provider ausgetauscht wird.


Zusammenstellung in cordis.yml

Die drei Pakete sind beim Schreiben nur Teile; Sie müssen Provider und Consumer außerdem zusammen in cordis.yml laden.

Das Laden des Providers gibt ctx.myCap eine Implementierung; das Laden des Consumers gibt dem Modell das my_cap-Tool.

Beispiel

# Dateipfad: cordis.yml
# Provider zuerst laden, damit ctx.myCap eine Implementierung hat.
- name
: '@deepseek-ai/dsh-my-cap-local'

Consumer neu laden, damit das Modell das my_cap-Tool verwenden kann
- name
: '@deepseek-ai/dsh-tool-my-cap'

Was die Ladereihenfolge betrifft, sortiert Cordis automatisch nach Abhängigkeit, daher spielt es keine Rolle, ob Provider oder Consumer zuerst kommt.

Um zu einem anderen Provider zu wechseln, ändern Sie nur die erste Zeile; der Consumer bleibt unverändert.


Designpunkte

Die offizielle Dokumentation bietet drei Designprinzipien; es ist am besten, sie sich vor dem Schreiben einer Capability bewusst zu machen.

Erstens: nicht vorsorglich aufteilen.

Rollen nur dann in separate Pakete packen, wenn sie sich unabhängig weiterentwickeln müssen.

Einfache Tool-Plugins müssen nicht aufgeteilt werden; es ist völlig legitim, dass ein Paket mehrere Rollen übernimmt.

Zweitens hat die Service Definition Request-/Result-Typen.

Provider und Consumer hängen nur vom Definition-Paket ab, daher müssen die Request- und Result-Typen von Definition definiert werden.

Drittens: Explizit ist besser als implizit.

Implementierungen sollten Defaults über einen expliziten resolve(request)-Schritt behandeln, statt einen ??-Default in run() zu verstecken.

Die Bash-Naht ist ein Beispiel: workdir und timeoutMs von ShellExecRequest sind optional; die Tool-Schicht ruft zuerst ctx.shell.resolve(request) auf, um eine ShellExecSpec mit allen Feldern als Pflichtfelder zu erhalten, und übergibt sie dann an run().

Defaults nicht stillschweigend in run() eintragen; erst auflösen, dann ausführen – nur dann sind die Grenzen klar.


Zusammenfassung und Selbsttest

In einem Satz: Eine austauschbare Fähigkeit schreiben = abstrakte Klasse Definition + Provider-Unterklasse + Consumer-Tool, plus eine cordis.yml.

Selbsttestfrage 1: In welchem Paket sollten MyCapRequest und MyCapResult definiert werden?

Selbsttest 2: Gibt es eine Abhängigkeit zwischen Provider und Consumer?

Selbsttest 3: Warum wird präventives Aufteilen für einfache Tool-Plugins nicht empfohlen?

Weitere Erweiterungen