CH 20 · defineTool: Agent를 위한 도구 만들기
본 장 목표
CH 18과 CH 19에서 작성한 플러그인은 로그만 남겼습니다 — 그것은 단지 "플러그인이 적재되었다"를 검증하기 위함이었습니다. 이 장에서는 플러그인의 진짜로 가치 있는 것, 즉 도구를 만듭니다.
도구는 Agent의 "손"입니다: 모델이 무언가를 말하면, 본인이 작성한 코드가 호출되어 실제 작업을 수행하고 그 결과를 다시 모델에게 돌려줍니다. 이전 장들에서 사용해 온 dsh의 내장 도구(파일 읽기, 명령어 실행, 검색)도 이 도구이며, 이 장에서는 직접 하나를 만들어 모델이 실제로 손을 뻗어 호출하도록 합니다.
도구는 플러그인의 핵심
매일 사용하시는 것을 떠올려 보세요: dsh의 Agent는 파일을 읽고, 파일을 쓰고, 명령을 실행할 수 있습니다 — 이것이 도구(ctx.tools에 등록된 능력들)입니다. 모델 자체는 단지 "말할 수 있을" 뿐이며, 도구가 그로 하여금 "행동"할 수 있게 합니다.
도구 플러그인의 최소 골격은 다음과 같습니다:
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
// 도구의 네 부분, 아래에서 하나씩 분해합니다
}))
}inject: ['tools']는 "도구 레지스트리를 사용하겠다"는 선언이며, CH 19에서 배웠습니다. ctx.tools.register(...)는 도구를 레지스트리에 마운트합니다.
defineTool의 네 부분
defineTool은 하나의 객체를 받아 dsh에게 "이 도구의 이름은 무엇이며, 언제 사용해야 하며, 어떤 파라미터가 필요하며, 실제 작업은 어떻게 수행되는지"를 알려줍니다. 도구 = Agent를 위한 "채용 JD":
| 필드 | 의미 | 비유 |
|---|---|---|
name | 도구의 이름, 모델이 이를 사용해 호출 | 직무명 |
description | 모델에게 "이 도구는 무엇을 하고, 언제 사용해야 하는지"를 알림 | 직무 책임 |
parameters | 어떤 파라미터가 필수이고 어떤 것이 선택인지 선언 | 제출할 서류 |
execute | 실제 작업을 수행하는 함수 | 입사 후 업무 |
완성된 최소 도구를 살펴봅니다(공식 튜토리얼과 동일, 약간 번역):
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet by name',
parameters: {
name: { type: 'string', required: true, description: 'Name of the person to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}네 가지 핵심 포인트:
parameters는 JSON Schema로 작성:type: 'string'은 파라미터 타입을 선언하고,required: true는 필수임을 선언합니다. 프레임워크가 모델이 전달한 파라미터를 자동으로 검증하며, 규격에 맞지 않으면 오류가 발생합니다 —execute에서 직접 타입 검사를 할 필요가 없습니다.execute(args)는 실제 작업을 수행하는 함수:args는 이미 검증되었으니 안심하고 사용하시면 됩니다. 이 함수는 "정준 값(canonical value)"을 반환합니다(여기서는 문자열).output.schema는 반환값의 형상을 선언하고,output.render는 반환값을 모델이 볼 수 있는 콘텐츠로 변환합니다(여기서는 텍스트 블록). 반환값은 먼저 정준 형태로 존재하며, 렌더링 계층이 그것을 모델용으로 "번역"하는 책임을 집니다.description은 매우 중요합니다: 모델은 이 설명을 근거로 "지금 이 도구를 써야 할까"를 판단합니다. 명확하고 구체적으로 작성할수록, 모델은 호출 시점을 올바르게 파악합니다.
실습: greet 도구를 작성해 모델이 실제로 호출하게 만들기
플러그인을 둘 워크스페이스에서 디렉터리를 만듭니다:
New-Item -ItemType Directory -Path "tool-demo\src" -Force1단계: 의존성 설치
defineTool은 @deepseek-ai/dsh-tools에서 오므로, 플러그인 디렉터리에서 먼저 설치합니다:
cd "E:\software-workspace\DeepSeek harness demo\tool-demo" # 본인 디렉터리로 바꾸세요
npm init -y
npm install @deepseek-ai/dsh-toolspackage.json에 "type": "module" 한 줄을 꼭 추가해야 함을 잊지 마세요(CH 19에서 언급, 없으면 경고가 줄줄이 출력됨).
2단계: 도구 작성
tool-demo\src\greet.js를 만들고, 위와 동일한 greet 도구를 작성합니다. 터미널에서 실제로 호출되었음을 쉽게 확인할 수 있도록 execute에 로그 한 줄을 추가했습니다:
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet by name',
parameters: {
name: { type: 'string', required: true, description: 'Name of the person to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
console.log('[greet] called with', args.name)
return `Hello, ${args.name}!`
},
}))
}3단계: 선언 및 검증
tool-demo\cordis.yml을 만듭니다(경로는 본인 것으로 바꾸고, 공백은 %20으로 적어야 함을 기억):
- insert:
- id: greet
name: 'file:///E:/your-workspace/tool-demo/src/greet.js'headless로 한 번 실행해 모델이 실제로 호출하도록 합니다:
cd your-workspace-directory
dsh --profile headless --patch "./tool-demo/cordis.yml" "You must call the greet tool, say hi to Ada, then tell me verbatim what the tool returned"터미널 출력에서 다음을 보게 됩니다:
[greet] called with Ada이는 execute가 모델에 의해 실제로 호출되었다는 증거입니다 — 본인의 코드가 모델의 손길에 의해 정말로 실행되었습니다. 그리고 모델의 답변에는 도구가 반환한 Hello, Ada!가 포함됩니다.

터미널의 노란색 [greet] called with Ada는 모델이 실제로 execute를 호출한 로그이고, 아래의 Hello, Ada!는 모델에 반환되는 도구의 결과입니다.
dsh에게 맡기기: 한 줄 프롬프트로 끝내기
도구 작성 절차는 플러그인 작성 절차와 완전히 동일하며, dsh가 직접 할 수 있고, defineTool의 필드와 schema 작성 방식을 본인보다 더 잘 알고 있습니다.
Web UI의 입력창에 다음 프롬프트를 그대로 보내세요:
현재 워크스페이스에서 도구 플러그인을 작성해 줘: defineTool로 가장 단순한 도구를 정의해 (필수 파라미터 하나, execute에서 텍스트 반환). 먼저 dsh 공식 문서를 읽어 defineTool의 필드와 파라미터 검증 규칙을 파악한 다음, 공식 명세에 따라 구현하고, headless를 실행해 모델이 실제로 호출하게 한 후, 마지막에 결과와 파일 위치를 알려 줘.dsh가 문서를 직접 찾아보고, 의존성을 설치하고, 도구를 작성하고, 모델이 실제로 호출했는지 검증합니다. 그저 확인하시면 됩니다.

이는 제가 실제로 이 프롬프트를 실행한 결과입니다: 문서를 직접 읽고, 플러그인의 세 부분(package.json의 bundle 선언, defineTool 진입 index.js, 한 줄짜리 insert cordis.patch.yml)을 만들었으며, Windows 경로에 공백이 있으면 설치 명령어 파싱이 깨진다는 문제까지 스스로 발견해, 공백이 없는 경로로 플러그인을 옮긴 다음 설치했습니다 — 함정을 스스로 만나서 스스로 우회한 셈입니다.

실행 후에는 함정까지 항목으로 정리했습니다: 플러그인 export 형태 요구사항, inject의 명시적 선언 필요, 워크스페이스 패키지는 실행을 위해 빌드 산출물이 필요함, 경로 공백 함정.
자주 겪는 함정
| 문제 | 현상 | 대응 |
|---|---|---|
Cannot find package '@deepseek-ai/dsh-tools' 보고 | 플러그인 디렉터리에 의존성을 설치하지 않음 | npm install @deepseek-ai/dsh-tools |
| 모델이 도구를 호출하지 않음 | description이 불명확하여 모델이 사용 시점을 모름 | description을 구체적으로 작성: "Use when user requests X" |
| 파라미터가 제대로 전달되지 않음 | schema와 모델의 이해가 맞지 않음 | parameters에서 각 파라미터의 description을 명확히 작성 |
| 도구가 파라미터 오류를 보고 | 모델이 잘못된 파라미터를 전달함 | 필수 항목을 required: true로 표시하고, 타입을 정확히 작성 |
| 모델이 호출은 했지만 결과가 잘못됨 | execute의 로직 오류 | execute에 console.log를 넣어 디버깅하고, 로그 확인 |
이 장에서 배운 것
아래 항목들을 직접 해낼 수 있으면 합격입니다:
- [ ] defineTool의 네 부분을 말할 수 있다:
name/description/parameters/execute - [ ]
parameters가 JSON Schema임을 알며, 프레임워크가 파라미터를 자동으로 검증함을 안다 - [ ] 도구 플러그인을 작성해
ctx.tools에 등록 - [ ] headless를 사용해 모델이 본인의 도구를 실제로 호출했는지 검증
- [ ]
description이 모델이 본인의 도구를 사용할 시점을 결정함을 안다
