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
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
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
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.
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
# 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