用 Cordis 从零构建一个 Mini DeepSeek Harness
ç¨ Cordis ä»é¶æå»ºä¸ä¸ª Mini DeepSeek Harness
minorcell 2026-08-15 0 é 读10åéåæ¥æ´æ°è³ä¸ªäººç«ç¹ï¼ç¨ Cordis ä»é¶æå»ºä¸ä¸ª Mini Harness
ä» hello world æä»¶å¼å§ï¼éæ¥ç¨æå¡ãä¾èµæ³¨å ¥åäºä»¶æå»ºåºä¸ä¸ªè½è°ç¨ DeepSeek APIãæ§è¡ bash/fetch/æä»¶æç´¢çè¿·ä½ Agent è¿è¡æ¡æ¶
ä¸ä¸ç¯ãDeepSeek Harness éè§ã讲äºâä¸åçæä»¶âçç念ï¼è¿ç¯å¨æï¼ç¨å®çåºåº§ Cordis ä»é¶æ¼åºä¸ä¸ª mini harness ââ å «ææä»¶ãä¸ä¸ªæå¡ã两个äºä»¶ã䏿¡ Agent 循ç¯ï¼æå让模åèªå·±æç´¢ä»£ç ãæ§è¡å½ä»¤ãåçä½ çé®é¢ã
第ä¸ç« · 第ä¸ä¸ªæä»¶
è¿ä¸ªæç¨å¨åä»ä¹
æä»¬è¦åçæ¯ä¸ä¸ª mini ç DeepSeek Harnessï¼ä¸ä¸ªè½è°ç¨æ¨¡åãæ§è¡çå®å·¥å ·ï¼shell å½ä»¤ãHTTP 请æ±ãæä»¶æç´¢ï¼ãè·é Agent 循ç¯çæå°è¿è¡æ¡æ¶ãå®åçå® Harness çå·®è·å¨æåä¸ç« æ»ç»ï¼ä½æ ¸å¿æºå¶å®å ¨ä¸è´ââå 为ç¨çå°±æ¯åä¸ä¸ªåºåº§ Cordisã
å æ³æ¸ æ¥âæ¼åºä¸ä¸ª harnessâæå³çä»ä¹ãä¸ä¸ç¯è®²è¿ï¼harness 没æå æ ¸ï¼å ¨é¨è½å齿¯æä»¶ãæä»¥æä»¬çæå»ºæ¹å¼ä¸æ¯âåä¸ä¸ªä¸»ç¨åºï¼åå¾éå åè½âï¼èæ¯æè½åææä¸æææä»¶ï¼æåç¨å è¡ä»£ç æå®ä»¬æ¼èµ·æ¥ãè¿ç¯æç¨çæ¯ä¸æ¥ï¼å°±æ¯å ¶ä¸ä¸ææä»¶ã
æåæ¼åºæ¥çä¸è¥¿é¿è¿æ ·ï¼
å
«ææä»¶ï¼ä¸ä¸ªæå¡ï¼ä¸¤ä¸ªäºä»¶ãmini-llmãmini-toolsãmini-agent æä¾æå¡ï¼ä¸ä¸ªå·¥å
·æä»¶ï¼bashãfetchãsearchï¼éè¿ inject ä¾èµ toolsï¼mini-prompt å tracer éè¿ ctx.on åä¸äºä»¶ã没æâ主ç¨åºâââåªææä»¶åå®ä»¬ä¹é´çå
³ç³»ã
åç½®è¦æ±
- Node.js ⥠18
- DeepSeek API Keyï¼å®ç½ç³è¯·ï¼ç¯å¢åé
DEEPSEEK_API_KEYï¼
æ¬æç¨ç宿´ä»£ç æ¯ä¸ä¸ªç¬ç«é¡¹ç®ï¼å°±å¨ä»åºç project/ ç®å½éï¼GitHub æºç ï¼ãå
éæ´ä¸ªä»åºåï¼è¿å
¥é¡¹ç®ç®å½è£
ä¾èµï¼
cd content/tutorials/cordis-mini-harness/project
npm install
注æå
忝 cordisâânpm ä¸è¿æä¸ä¸ª @cordisjs/coreï¼é£æ¯ v3 æ¶ä»£çéçå
åï¼å«è£
éãè¿è¡ TypeScript ç¨ tsxï¼Cordis èªèº«çæ¨¡åæ¯æ æ©å±å导å
¥ï¼å¿
é¡»ç» tsx ææå
å¨è¿è¡ï¼åç Node è·ä¸äºï¼ï¼é¡¹ç®éå·²é
好 npm start èæ¬ã
æå°æä»¶é¿ä»ä¹æ ·
import { Context } from 'cordis'
const app = new Context()
app.plugin({
name: 'hello',
apply(ctx: Context) {
console.log('hello from my first plugin')
},
})
æè¿æ®µä¿å为任æä¸ä¸ª .ts æä»¶ï¼æ¯å¦ hello.tsï¼ï¼ç¨ npx tsx hello.ts è·ä¸ä¸ï¼æ§å¶å°ä¼æåº hello from my first pluginãè¿ä¸ªæä»¶åªæ¯ç身ï¼ä¸å±äºæç»é¡¹ç®ã
ä¸ä¸ª Cordis åºç¨å°±æ¯ ä¸ä¸ª Contextãnew Context() åå»ºæ ¹ä¸ä¸æï¼app.plugin(...) ææä»¶æä¸å»ãæä»¶æ¯ä¸ä¸ªå¸¦ name å apply(ctx) ç对象ââname æ¯ç»è¯æä¿¡æ¯ç¨çæ è¯ï¼apply æ¯å
¥å£ï¼æ¡æ¶å¨å è½½æ¶è°ç¨å®ï¼å¹¶æä¸ä¸æäº¤å°ä½ æéã
æä»¶æä¸ç§å½¢æï¼å½æ°ï¼ç´æ¥å applyï¼ã对象ï¼åä¸é¢è¿æ ·å¸¦å段ï¼ãç±»ï¼Service åç±»ï¼åé¢ä¸¤æ¥ä¼ç¨å°ï¼ãä¸è
è¿è¡æ¶çä»·ï¼éåªç§åå³äºä½ è¦ä¸è¦éå¸¦ç¶æã
第äºç« · æå¡ï¼æè½åæä¸ ctx
为ä»ä¹éè¦æå¡
hello æä»¶åªè¯æäºä¸ä»¶äºï¼æä»¶è½è¢«å è½½ãä½ä¸ä¸ª harness éè¦ç䏿¯âåèªä¸ºæ¿ç代ç âï¼èæ¯å¯è¢«å ¶ä»æä»¶åç°å使ç¨çè½åãCordis ççæ¡å«æå¡ï¼Serviceï¼ï¼æä»¶æè½åæå°ä¸ä¸æä¸ï¼å ¶ä»æä»¶æååæ¾å®ï¼è䏿¯ import å®çå ·ä½å®ç°ã
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
llm: LlmService
}
}
export interface LlmConfig {
model?: string
baseUrl?: string
}
export interface ChatMessage {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string
}
export class LlmService extends Service {
config: Required<LlmConfig>
constructor(ctx: Context, config: LlmConfig = {}) {
super(ctx, 'llm')
this.config = {
model: config.model ?? 'deepseek-v4-flash',
baseUrl: config.baseUrl ?? 'https://api.deepseek.com',
}
}
async chat(messages: ChatMessage[]): Promise<string> {
const res = await fetch(`${this.config.baseUrl}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`,
},
body: JSON.stringify({
model: this.config.model,
messages,
}),
})
if (!res.ok) {
throw new Error(`LLM è°ç¨å¤±è´¥ï¼${res.status} ${await res.text()}`)
}
const data = await res.json()
return data.choices[0].message.content as string
}
}
export const name = 'mini-llm'
export function apply(ctx: Context, config?: LlmConfig) {
ctx.plugin(LlmService, config)
}
è¿å°±æ¯é¡¹ç®éç llm.tsââharness çâæ¨¡åè°ç¨âè½åï¼ä¸è¡è¡çï¼
第 3-7 è¡ç declare module æ¯ TypeScript ç声æåå¹¶ï¼å®åè¯ç±»åç³»ç»âctx ä¸ä»æ¤å¤äºä¸ä¸ª llm 屿§âãæ²¡æè¿è¡æ¶å¯ä½ç¨ï¼ä½æäºå®ï¼ä»»ä½æ¿å° Context çå°æ¹é½è½å¸¦ç±»åå°ååº ctx.llm.chat(...)ââè¿å°±æ¯ä¸ä¸ç¯æç« 说çâéè¿ key æ¾æå¡ï¼è䏿¯ import å
·ä½å®ç°âçç±»ååºç¡ã
LlmService extends Service æ¯æä»¶ä¸æå¡çåä½ï¼å®æ¯æä»¶ï¼å¯ä»¥è¢« ctx.plugin() æè½½ï¼ï¼åæ¶éè¿ super(ctx, 'llm') æèªå·±æ³¨åæå为 llm çæå¡ã注ååçå¨æé æ¶ï¼å¸è½½æ¶èªå¨ç§»é¤ââæå¡æ³¨åæ¬èº«å°±æ¯ä¸ä¸ªå¯éçå¯ä½ç¨ã
é
置走 apply(ctx, config) ç第äºåæ°ï¼model é»è®¤ deepseek-v4-flashï¼æ§æ¨¡åå deepseek-chat å·²äº 2026 å¹´ 7 æåç¨ï¼éè¦æ´å¼ºæ¨çæ¶æ¢æ deepseek-v4-proï¼ï¼baseUrl é»è®¤å®æ¹ API å°åãè¿éåªåäºé»è®¤å¼åå¹¶ï¼çå®ç Cordis çæç¨ Schemasteryï¼æ å Schemaï¼åè¿è¡æ¶æ ¡éªï¼æç¨åºæ¯ä»ç®ã
chat() æ¹æ³å°±æ¯ä¸æ¬¡æ åç OpenAI å
¼å®¹è¯·æ±ï¼è¿å模ååå¤ççº¯ææ¬ãå®è¿ä¸ç¥éå·¥å
·æ¯ä»ä¹ï¼é£æ¯ä¸ä¸æ¥çäºã
第ä¸ç« · å·¥å ·æ³¨å表
å åæ³¨å表ï¼å注åå·¥å ·
import { Service, type Context } from 'cordis'
declare module 'cordis' {
interface Context {
tools: ToolsService
}
}
export interface Tool {
name: string
description: string
parameters: Record<string, unknown>
execute(args: Record<string, unknown>): Promise<string> | string
}
export class ToolsService extends Service {
private tools = new Map<string, Tool>()
constructor(ctx: Context) {
super(ctx, 'tools')
}
register(tool: Tool) {
this.tools.set(tool.name, tool)
}
describe(): string {
return [...this.tools.values()]
.map(
(tool) =>
`- ${tool.name}ï¼${tool.description}ï¼åæ° ${JSON.stringify(tool.parameters)}`,
)
.join('\n')
}
async execute(name: string, args: Record<string, unknown>): Promise<string> {
const tool = this.tools.get(name)
if (!tool) throw new Error(`æªç¥å·¥å
·ï¼${name}`)
return await tool.execute(args ?? {})
}
}
export const name = 'mini-tools'
export function apply(ctx: Context) {
ctx.plugin(ToolsService)
}
è¿å°±æ¯ tools.tsãå®å®ä¹äºä¸ä¸ªå·¥å
·åè®®ï¼æ¯ä¸ªå·¥å
·è¦æ nameãdescriptionãparametersï¼JSON Schema çæ®µï¼åç»æ¨¡åççï¼åä¸ä¸ª execute 彿°ã
ToolsService æ¯ä¸ä¸ªæ³¨å表ï¼register() ç»è®°å·¥å
·ï¼describe() æå·²ç»è®°çå·¥å
·æ¸²ææä¸æ®µç»æ¨¡åççæ¸
åï¼execute() æååæ§è¡ã注æå®ä¸æä¾ä»»ä½å
·ä½å·¥å
·ââå®åªè´è´£âè½æ³¨åãè½æ§è¡âè¿ä»¶äºã
å ·ä½å·¥å ·ä»åªæ¥ï¼ä¸ä¸ç« ç䏿æä»¶ã
第åç« Â· ä¸ä¸ªçå®å·¥å ·
å·¥å ·ä¸æ¯é ç½®é¡¹ï¼æ¯æä»¶
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import type { Context } from 'cordis'
const execAsync = promisify(exec)
export const name = 'mini-bash'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register({
name: 'bash',
description: 'æ§è¡ä¸æ¡ shell å½ä»¤å¹¶è¿åè¾åºï¼ä»
鿬æºï¼è¶
æ¶ 10 ç§ï¼',
parameters: {
type: 'object',
properties: {
command: { type: 'string', description: 'è¦æ§è¡çå½ä»¤' },
},
required: ['command'],
},
async execute(args) {
const command = String(args.command ?? '')
try {
const { stdout, stderr } = await execAsync(command, { timeout: 10_000 })
return (stdout + stderr).trim() || '(æ è¾åº)'
} catch (error) {
return `å½ä»¤æ§è¡å¤±è´¥ï¼${(error as Error).message}`
}
},
})
}
è¿å°±æ¯ bash.tsâââä¸åçæä»¶âå¨å·¥å
·å±é¢çå«ä¹ï¼å·¥å
·ä¸æ¯æ³¨å表çé
置项ï¼èæ¯ç¬ç«çæä»¶ãmini-bash ç¨ inject: ['tools'] 声æèªå·±ä¾èµ tools æå¡ââæ¡æ¶çå°è¿è¡ï¼ä¼ç tools 就绪åæå è½½å®ï¼å¯å¨é¡ºåºç±ä¾èµæ¨å¯¼ï¼ä¸éè¦ä»»ä½ç¼æä»£ç ã
å®ç°ä¸æ³¨æä¸¤ç¹ï¼execAsync å
ä¸å± promisifyï¼è®¾ 10 ç§è¶
æ¶é²æ¢å½ä»¤ææ»ï¼stdout å stderr æ¼å¨ä¸èµ·è¿åãé误ä¸å¾å¤æï¼èæ¯ä½ä¸ºå符串è¿åç»æ¨¡åââå·¥å
·å¤±è´¥ä¹æ¯ä¸æ¡ä¿¡æ¯ï¼æ¨¡åè½æ®æ¤è°æ´çç¥ï¼æå¼å¸¸åªä¼è®©å¾ªç¯å´©æºã
å¦å¤ä¸¤ææ¨¡å¼å®å ¨ç¸åï¼å¯ä»¥å¯¹ç §ççï¼
import type { Context } from 'cordis'
export const name = 'mini-fetch'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register({
name: 'fetch',
description: '请æ±ä¸ä¸ª http/https URL å¹¶è¿åç¶æç åææ¬å
容',
parameters: {
type: 'object',
properties: {
url: { type: 'string', description: 'è¦è¯·æ±ç URL' },
method: { type: 'string', description: 'HTTP æ¹æ³ï¼é»è®¤ GET' },
body: { type: 'string', description: '请æ±ä½ï¼JSON å符串ï¼å¯éï¼' },
},
required: ['url'],
},
async execute(args) {
const url = String(args.url ?? '')
const method = String(args.method ?? 'GET')
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: args.body ? String(args.body) : undefined,
})
const text = await res.text()
return `${res.status}\n${text.slice(0, 2000)}`
},
})
}
fetch 请æ±ä»»æ http/https URLï¼è¿åç¶æç åææ¬ï¼æªæå° 2000 å符ï¼ã注æ body åªå¨æå¼æ¶æä¼ ï¼é¿å
GET 请æ±å¸¦ç©º bodyã
import { readdir, readFile } from 'node:fs/promises'
import { join, extname } from 'node:path'
import type { Context } from 'cordis'
const TEXT_EXT = new Set([
'.ts', '.tsx', '.js', '.mjs', '.cjs', '.json', '.md', '.txt', '.yml', '.yaml', '.html', '.css',
])
async function searchFiles(dir: string, keyword: string, depth = 3): Promise<string[]> {
if (depth < 0) return []
const results: string[] = []
const entries = await readdir(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
const full = join(dir, entry.name)
if (entry.isDirectory()) {
results.push(...(await searchFiles(full, keyword, depth - 1)))
} else if (TEXT_EXT.has(extname(entry.name))) {
const content = await readFile(full, 'utf8').catch(() => '')
if (content.includes(keyword)) results.push(full)
}
}
return results
}
export const name = 'mini-search'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register({
name: 'file_search',
description: 'å¨ç®å½ä¸æç´¢å
å«å
³é®è¯çææ¬æä»¶ï¼è·³è¿ node_modules åéèç®å½ï¼',
parameters: {
type: 'object',
properties: {
keyword: { type: 'string', description: 'è¦æç´¢çå
³é®è¯' },
dir: { type: 'string', description: 'èµ·å§ç®å½ï¼é»è®¤å½åç®å½' },
},
required: ['keyword'],
},
async execute(args) {
const keyword = String(args.keyword ?? '')
const dir = String(args.dir ?? process.cwd())
try {
const found = await searchFiles(dir, keyword)
return found.length ? found.slice(0, 20).join('\n') : 'æ²¡ææ¾å°å¹é
æä»¶'
} catch (error) {
return `æç´¢å¤±è´¥ï¼${(error as Error).message}`
}
},
})
}
file_search éå½æç´¢ææ¬æä»¶ï¼è·³è¿ node_modules åéèç®å½ï¼æ·±åº¦éå¶ä¸å±ï¼ï¼è¿åå½ä¸æä»¶å表ãè¿æ¯ Agent æå¸¸ç¨çå·¥å
·ç±»åï¼å
å®ä½ï¼å读æä»¶ã
ä¸ä¸ªå·¥å ·åèµ·æ¥ï¼æ¨¡åå°±æäºä¸å¥åºç¡çâæèâï¼æ§è¡å½ä»¤ã访é®ç½ç»ãæç´¢ä»£ç ãå·¥å ·ç夿 ·æ§ä¸é 注å表设计ï¼é æä»¶æ°éââæ³å ä»ä¹å·¥å ·ï¼å°±ç §è¿ä¸ªæ¨¡æ¿åå䏿æä»¶ï¼æ³ç¦ç¨ä»ä¹å·¥å ·ï¼å°±ä»è£ é æ¸ åéææä¸è¡ã
第äºç« · æç¤ºè¯ä¹æ¯ä¸ææä»¶
æç¡¬ç¼ç çæç¤ºè¯æåºå»
ä¸ä¸æ¥å Agent æ¶ï¼æå®¹æè¢«åæ»çå°±æ¯æç¤ºè¯ãçå® Harness çåæ³æ¯ææç¤ºè¯åæç¬ç«æä»¶ï¼@deepseek-ai/dsh-system-promptï¼ï¼å 为æç¤ºè¯éè¦è¢«å
¶ä»æä»¶ä¿®æ¹ï¼å è§åçãå ä¸ä¸æçãå å·¥å
·æè¿°çï¼åç®¡ä¸æ®µï¼è°ä¹ä¸è®¤è¯è°ã
mini çç §åï¼
import type { Context } from 'cordis'
export const name = 'mini-prompt'
export const inject = ['tools']
const RULES = `åçè§åï¼
- éè¦è°ç¨å·¥å
·æ¶ï¼åªè¾åºä¸è¡ JSONï¼{"tool": "å·¥å
·å", "args": {...}}
- ä»»å¡å·²ç»å®ææ¶ï¼åªè¾åºä¸è¡ JSONï¼{"answer": "ç»ç¨æ·çæç»åç"}
- ä¸è¦è¾åº JSON ä¹å¤çä»»ä½å
容ã`
export function apply(ctx: Context) {
ctx.on('prompt/build', async (base, next) => {
const downstream = await next()
return `${downstream}\n\nå¯ç¨å·¥å
·ï¼\n${ctx.tools.describe()}\n\n${RULES}`
})
}
è¿å°±æ¯ prompt.tsãå®çå¬ prompt/build äºä»¶ââæ³¨æè¿ä¸æ¯æ®é广æï¼èæ¯ waterfall 模å¼ï¼çå¬å¨æ¶å°åæ°åä¸ä¸ª next() ç»å»¶ï¼await next() æ¿å°ä¸æ¸¸çç»æï¼åå
è£
åè¿åç»ä¸æ¸¸ãè¿éç䏿¸¸æ¯ agent æä¾çâåºç¡æç¤ºè¯âï¼mini-prompt å¨å®åé¢è¿½å å·¥å
·æ¸
åååçè§åã
æ³è¦åå 䏿®µæç¤ºè¯ï¼åå 䏿æä»¶çå¬åä¸ä¸ªäºä»¶å°±è¡ï¼ä¸ç¨æ¹ä»»ä½å·²æä»£ç ãæç¤ºè¯ä»ä¸æ®µå符串åæäºä¸æ¡å¯ææçå¤çé¾ã
ï¼waterfall çå¦ä¸ä¸ªè¯ä¹æ¯çè·¯ï¼ä¸è° next() ç´æ¥è¿åï¼ä¸æ¸¸å°±æ¶ä¸å°äºãå¯¹âæ¦æª/å¦å³âç±»äºä»¶è¿æ¯è®¾è®¡æå¾ï¼å¯¹â追å /修饰âç±»äºä»¶åæ¯çºªå¾ââæ°¸è¿è®°å¾è° next()ãï¼
第å ç« Â· Agent 循ç¯
æå¤§èæ¥è¿èº«ä½
ç°å¨æäºâè½è天âç llmãä¸ä¸ªçå®å·¥å
·ã䏿¡å¯ææçæç¤ºè¯é¾ï¼è¿å·®ä¸ä¸ªæå®ä»¬è¿èµ·æ¥ç循ç¯ãè¿å°±æ¯ Agent çæ ¸å¿ï¼æ¨¡åè¾åºå·¥å
·è°ç¨ â ä»£ç æ§è¡ â ç»æåå¡« â æ¨¡åç»§ç»å³çï¼ç´å°ç»åºæç»åçã
import { Service, type Context } from 'cordis'
import type { ChatMessage } from './llm.js'
declare module 'cordis' {
interface Context {
agent: AgentService
}
interface Events {
'agent/step'(step: AgentStep): void
'prompt/build'(base: string, next: () => Promise<string>): Promise<string>
}
}
export type AgentStep =
| { type: 'tool'; tool: string; args: Record<string, unknown>; result: string }
| { type: 'answer'; answer: string }
const BASE_PROMPT = 'ä½ æ¯ä¸ä¸ªå¯ä»¥è°ç¨å·¥å
·ç agentã'
function parseJson(text: string): Record<string, any> | null {
const stripped = text.trim().replace(/^```(?:json)?\s*|\s*```$/g, '')
try {
return JSON.parse(stripped)
} catch {
return null
}
}
export class AgentService extends Service {
constructor(ctx: Context) {
super(ctx, 'agent')
}
async buildPrompt(): Promise<string> {
return this.ctx.waterfall('prompt/build', BASE_PROMPT, async () => BASE_PROMPT)
}
async run(prompt: string, maxTurns = 5): Promise<string> {
const messages: ChatMessage[] = [
{ role: 'system', content: await this.buildPrompt() },
{ role: 'user', content: prompt },
]
for (let turn = 0; turn < maxTurns; turn++) {
const reply = await this.ctx.llm.chat(messages)
const parsed = parseJson(reply)
if (!parsed) {
return reply
}
if (parsed.answer) {
this.ctx.emit('agent/step', { type: 'answer', answer: parsed.answer })
return parsed.answer
}
if (parsed.tool) {
const result = await this.ctx.tools.execute(parsed.tool, parsed.args)
this.ctx.emit('agent/step', {
type: 'tool',
tool: parsed.tool,
args: parsed.args ?? {},
result,
})
messages.push({ role: 'assistant', content: reply })
messages.push({ role: 'user', content: `å·¥å
·ç»æï¼${result}` })
continue
}
return reply
}
return 'å·²è¾¾å°æå¤§å¾ªç¯è½®æ°ï¼ä»»å¡ä¸æ¢ã'
}
}
export const name = 'mini-agent'
export const inject = ['llm', 'tools']
export function apply(ctx: Context) {
ctx.plugin(AgentService)
}
è¿å°±æ¯ agent.tsãå
³é®è®¾è®¡æååï¼
äºä»¶ç±»å声æï¼ç¬¬ 4-12 è¡ï¼ãagent/step æ¯å¹¿æäºä»¶ï¼emitï¼çº¯éç¥ï¼ï¼prompt/build æ¯ waterfall äºä»¶ââæ³¨æå®çç¾å带 next: () => Promise<string>ï¼è¿æ¯ waterfall çå¬å¨çæ å¿ï¼ç±»åç³»ç»é å®åºå模å¼ã
æç¤ºè¯æ¥èªäºä»¶é¾ï¼ç¬¬ 34-36 è¡ï¼ãbuildPrompt() è°ç¨ ctx.waterfall('prompt/build', BASE_PROMPT, async () => BASE_PROMPT)ï¼BASE_PROMPT æ¯ç»ç¬¬ä¸ä¸ªçå¬å¨çåå§å¼ï¼æåä¸ä¸ªåæ°æ¯â没æçå¬å¨æ¶çå
åºå¼âãagent ä¸ç¥éæç¤ºè¯æç»é¿ä»ä¹æ ·ï¼ä¹ä¸ç¥éæè°å¨æ¹å®ã
循ç¯å¨ run() éï¼ç¬¬ 38-70 è¡ï¼ãæ¯è½®ï¼è°æ¨¡å â è§£æ JSON â æ answer å°±è¿åï¼æ tool å°±æ§è¡ãæç»æä½ä¸º user æ¶æ¯åå¡«ãè¿å
¥ä¸ä¸è½®ãmaxTurns = 5 æ¯å®å
¨éï¼é²æ¢æ¨¡åå¨å·¥å
·ä¸æè½¬ãè§£æå¤±è´¥æ¶ç´æ¥æåæäº¤ç»ç¨æ·ââæ¨¡å䏿åè®®åºç䏿¯éè¯¯ï¼æ¯å¸¸æï¼è¦æå
åºã
æ¯æ¥é½åäºä»¶ï¼ç¬¬ 52ã57 è¡ï¼ãå·¥å
·è°ç¨åæç»åçé½ä¼ ctx.emit('agent/step', ...) 广æåºå»ãAgent èªå·±ç¨ä¸ä¸å®ï¼ä½æ¥å¿ãç»è®¡ã鲿³æ¼è¿æ»¤å¨è¿äºè½åå¯ä»¥å¨ä¸æ¹ agent 代ç çåæä¸æ¥å
¥ãè¿å°±æ¯ä¸ä¸ç¯è¯´çï¼æ¦æªç¨äºä»¶ï¼è°ç¨ç¨æå¡ã
第ä¸ç« · ç»è£ ä¸è¿è¡
å «ææä»¶ï¼æ¼æä¸ä¸ª harness
import { Context } from 'cordis'
import * as readline from 'node:readline/promises'
import * as llm from './llm.js'
import * as tools from './tools.js'
import * as bash from './bash.js'
import * as fetchTool from './fetch.js'
import * as search from './search.js'
import * as prompt from './prompt.js'
import * as agent from './agent.js'
const tracer = {
name: 'tracer',
apply(ctx: Context) {
ctx.on('agent/step', (step) => {
if (step.type === 'tool') {
console.log(` [tool] ${step.tool}(${JSON.stringify(step.args)}) => ${step.result}`)
}
})
},
}
const app = new Context()
await app.plugin(llm)
await app.plugin(tools)
await app.plugin(bash)
await app.plugin(fetchTool)
await app.plugin(search)
await app.plugin(prompt)
await app.plugin(agent)
await app.plugin(tracer)
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
console.log('mini harness 已就绪ï¼è¾å
¥ exit éåºã')
while (true) {
const line = await rl.question('> ')
if (!line.trim()) continue
if (line === 'exit') break
const reply = await app.agent.run(line)
console.log(reply)
}
rl.close()
è¿å°±æ¯ bin.tsã第 11-21 è¡ç tracer æ¯ç¬¬å
«ææä»¶ï¼ä¹æ¯âäºä»¶çä»·å¼âçç°åºæ¼ç¤ºï¼å®çå¬ agent/stepï¼ææ¯æ¬¡å·¥å
·è°ç¨æå°æä¸è¡ [tool] æ¥å¿ãå®ä¸è®¤è¯ agentï¼agent ä¹ä¸è®¤è¯å®ââå®ä»¬å¯ä¸çäº¤éæ¯é£ä¸ªç±»ååäºä»¶ãæ tracer 仿¸
åéå æï¼agent ä¾ç¶å·¥ä½ï¼æ¢æåæä»¶ãåéç¥ççå¬å¨ï¼agent 乿µç¶ä¸è§ã
第 24-32 è¡å°±æ¯æ´ä¸ª harness çè£
é
æ¸
åï¼å
«ææä»¶æé¡ºåºæè½½ï¼é¡ºåºå
¶å®ä¸éè¦ï¼inject å·²ç»ä¿è¯äºçæ£çä¾èµé¡ºåºï¼ãawait 让æ¯ä¸ªæä»¶å è½½å®æåæç»§ç»ã
å©ä¸çæ¯ä¸ä¸ª readline 交äºå¾ªç¯ãè·èµ·æ¥ï¼
npm start
çå®è¾åºï¼DeepSeek APIï¼é®å®âå¨å½åç®å½æä¸ä¸åªäºæä»¶éæå° waterfallâï¼ï¼
mini harness 已就绪ï¼è¾å
¥ exit éåºã
> å¨å½åç®å½æä¸ä¸åªäºæä»¶éæå° waterfall
[tool] file_search => agent.ts
[tool] bash => 35: return this.ctx.waterfall('prompt/build', ...
å¨å½åç®å½ä¸ï¼agent.ts 第 35 è¡æå°äº waterfallã
宿´é¾è·¯ï¼æ¨¡åè¯»å°æç¤ºè¯é¾æå»ºåºçå·¥å
·æ¸
å â 夿âéè¦æç´¢â â file_search å®ä½å° agent.ts â ç¨ bash 精确å®ä½è¡å· â 综åä¸¤è½®ç»æåçãtracer é£ä¸¤è¡ [tool] æ¥å¿å°±æ¯ emit çç°åºã
宿¯ä»ä¹ï¼çå½å¨æçº§çä¸é´ä»¶ç³»ç»
åå°è¿éï¼å¯ä»¥åçâCordis å°åºåä»ä¹âäºã
å¦æä½ åè¿ Expressï¼ä¼è§å¾å®çæä»¶å¾ç¼çââ齿¯ä¸ä¸ªæ¥ä¸ä¸ªçå¤çå±ã使ä¸ä¸ªå ³é®åºå«ï¼Express çä¸é´ä»¶æ´»å¨ä¸æ¬¡è¯·æ±éï¼request è¿æ¥ â ç©¿è¿å±å±ä¸é´ä»¶ â response åºå»ï¼ï¼è Cordis çæä»¶æ´»å¨æ´ä¸ªåºç¨ççå½å¨æéï¼å¯å¨æ¶æä¾èµé¡ºåºå è½½ãæä¾æå¡åçå¬å¨ï¼å¸è½½æ¶éåºæ¸ çï¼è´¯ç©¿åºç¨å ¨ç¨ã
æ´æææçæ¯ Cordis å
é¨è¿æç¬¬äºå±ï¼waterfall äºä»¶å¨æ¯æ¬¡ååæ¶ææä¸æ¡ next() é¾ââçå¬å¨å
è£
ãä¿®æ¹ãçè·¯ï¼ç´å°ç»å¼ãè¿ä¸å±æçæ£å¯¹åº Express ä¸é´ä»¶çâ请æ±çº§âä½éªï¼æ¯å¦ mini-prompt 对æç¤ºè¯çå
è£
ã
æä»¥ä¸¥æ ¼å°è¯´ï¼
| ä½ç¨å | ä¾å | |
|---|---|---|
| Cordis æä»¶ | åºç¨çå½å¨æ | mini-llmãmini-agentãtracer |
| Cordis äºä»¶é¾ | æ¯æ¬¡åå | prompt/build çå è£ é¾ |
| Express ä¸é´ä»¶ | æ¯æ¬¡è¯·æ± | æ¥å¿ãé´æãè·¯ç± |
**æä»¶æ¯çå½å¨æçä¸é´ä»¶ï¼äºä»¶é¾æ¯ååçä¸é´ä»¶ã**çè§£äºè¿ä¸¤å±ï¼Cordis çâä¸åçæä»¶âå°±ä¸åæ¯å£å·ï¼èæ¯ä¸ä¸ªå ·ä½çå·¥ç¨æ¨¡åã
å®ç¦»çå® Harness è¿å·®ä»ä¹
è¯å®å°è¯´ï¼å·®å¾è¿å¾å¤ï¼
- ä¼è¯è®°å¿ï¼
ctx.sessionsä¹äºçå® Harnessï¼ï¼mini çæ¯æ¬¡run()齿¯å ¨æ°å¯¹è¯ - æµå¼è¾åºï¼
chat()æ¯éæµå¼çï¼çå® Harness çctx.llmæ¯æµå¼æå¡ - åçå·¥å
·åè®®ï¼ææ¬ JSON åè®®ç®åéæï¼ä½æ¯ä¸ä¸ function calling ç鲿£æ§ââåé¢çæ¼ç¤ºéï¼æ¨¡åå¶å°ä¼ææèæåå JSON æ··å¨ä¸èµ·è¾åºï¼æå¨å¼æ¾é®é¢ä¸æç»æ¢ç´¢ç´å°è§¦å
maxTurnså®å ¨é - é
置系ç»ï¼çå®çæç¨ Schemastery åè¿è¡æ¶æ ¡éªï¼è¿æ loader ç
!!jsè¡¨è¾¾å¼ - å®å ¨ä¸æ²»çï¼å·¥å ·ç½ååãå½ä»¤æ¦æªãææä¿¡æ¯è¿æ»¤ï¼å¨çå® Harness 鿝ç¬ç«æä»¶
ä½éª¨æ¶æ¯åä¸å¯ï¼è½åæææä»¶ãæä»¶éè¿æå¡åä½ãåä½è¿ç¨éè¿äºä»¶å¯è§ãçè§£äºè¿å¯éª¨æ¶ï¼å»ççå® Harness çæºç ï¼vendor/ ç®å½ä¸ç @deepseek-ai/cordisï¼ï¼å°±ä¸åæ¯ç天书äºã
æ³ç»§ç»æ·±å ¥ï¼ä»åºéç Cordis æç¨ï¼ä¸ç« ï¼å宿¹ææ¡£ç«æ¯ä¸ä¸ç«ã
Aitishiku.com