9.2 KiB
9.2 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ai-tool-composition-deep | Tool Composition — agent 가 tool 사용 / chain | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Tool Composition
Agent 의 능력 = tool. 잘 design 된 tool = clear name, type, doc, error. Tool chain (output → input). MCP 가 표준 protocol.
📖 핵심 개념
- Tool = 함수 + 명시적 schema.
- LLM 가 정확 tool + arg 호출.
- Composition: tool A → output → tool B.
- ReAct: reason + action loop.
💻 코드 패턴
단순 tool (Anthropic)
const tools = [
{
name: 'get_weather',
description: 'Get current weather for a city',
input_schema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name, e.g. "Seoul"' },
unit: { type: 'string', enum: ['c', 'f'], default: 'c' },
},
required: ['city'],
},
},
];
const r = await client.messages.create({
model: 'claude-opus-4-7',
tools,
messages: [{ role: 'user', content: 'Weather in Seoul?' }],
});
if (r.stop_reason === 'tool_use') {
const toolUse = r.content.find(c => c.type === 'tool_use');
const result = await executeWeather(toolUse.input);
// Tool result 반환
const r2 = await client.messages.create({
messages: [
{ role: 'user', content: 'Weather in Seoul?' },
{ role: 'assistant', content: r.content },
{ role: 'user', content: [{ type: 'tool_result', tool_use_id: toolUse.id, content: result }] },
],
});
}
Tool 정의 가이드
Name: snake_case, 동작 (get_, list_, create_).
Description: 이 tool 가 언제 / 무엇 / 다른 tool 와 차이.
Input schema: required 명시, default 명시.
Return: 명확.
// ❌ 모호
{
name: 'data',
description: 'Get data',
input_schema: { ... }
}
// ✅
{
name: 'list_users',
description: 'List users in the org. Use for finding existing users; for creating new users, use create_user.',
input_schema: {
type: 'object',
properties: {
limit: { type: 'integer', default: 50, description: 'Max users to return' },
filter: { type: 'string', description: 'Optional name/email filter' },
},
},
}
Tool chain
// User: "Send Alice the latest sales report"
// Step 1: LLM 가 find_user(name='Alice')
// Step 2: get_sales_report() → URL
// Step 3: send_email(to=alice.email, attachment=url)
// Agent loop
while (true) {
const r = await llm.call(messages);
if (r.stop_reason === 'end_turn') break;
if (r.stop_reason === 'tool_use') {
const result = await executeTool(r.tool_use);
messages.push({ role: 'tool', content: result });
}
}
Parallel tools
// Anthropic / OpenAI 가 1 turn 에 여러 tool 호출 가능
const tools = r.content.filter(c => c.type === 'tool_use');
const results = await Promise.all(tools.map(t => execute(t)));
messages.push({
role: 'user',
content: results.map((r, i) => ({ type: 'tool_result', tool_use_id: tools[i].id, content: r })),
});
→ "Get weather in Seoul AND Tokyo" → 2 tool 동시.
ReAct (reason + act)
Thought: I need user's email first.
Action: find_user(name='Alice')
Observation: { email: 'alice@x.com', ... }
Thought: Now I get the report.
Action: get_sales_report()
Observation: { url: 'https://...' }
Thought: Send email.
Action: send_email(to='alice@x.com', attachment='https://...')
Observation: { sent: true }
Thought: Task complete.
→ LLM 가 think → act → observe.
Tool error handling
async function executeTool(toolUse) {
try {
return await tools[toolUse.name](toolUse.input);
} catch (e) {
return {
type: 'tool_result',
tool_use_id: toolUse.id,
is_error: true,
content: `Error: ${e.message}. Try with different input.`,
};
}
}
→ LLM 가 retry / 다른 input.
권한 / 안전
const ALLOWED_TOOLS = {
read_file: ['*.md', '!secret/*'],
write_file: ['draft/*'],
shell: false, // disabled
};
function isAllowed(tool, args) {
if (tool === 'write_file') {
return micromatch.isMatch(args.path, ALLOWED_TOOLS.write_file);
}
// ...
}
→ Tool 가 user data destruction 방지.
Confirmation (destructive)
async function executeTool(toolUse) {
if (DESTRUCTIVE.includes(toolUse.name)) {
const ok = await confirmUser(`${toolUse.name}(${JSON.stringify(toolUse.input)})?`);
if (!ok) return { is_error: true, content: 'User cancelled' };
}
return tools[toolUse.name](toolUse.input);
}
MCP (Model Context Protocol)
// MCP server (Anthropic 표준)
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
const server = new Server({ name: 'my-tools', version: '1.0.0' });
server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: [
{ name: 'get_user', description: '...', inputSchema: { ... } },
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === 'get_user') {
return { content: [{ type: 'text', text: JSON.stringify(await getUser(...)) }] };
}
});
→ 표준 protocol — Claude Desktop, Claude Code 가 native 지원.
Tool registry / discovery
class ToolRegistry {
private tools = new Map<string, Tool>();
register(tool: Tool) { this.tools.set(tool.name, tool); }
list(): Tool[] { return [...this.tools.values()]; }
// 동적 — context 에 맞는 tool 만
filter(context: string): Tool[] {
return this.list().filter(t => t.relevantTo(context));
}
}
→ 100+ tool = context window 넘김. 필요한 것만.
Sub-agent (delegation)
const subAgent = {
name: 'research_subagent',
description: 'Delegate complex research task. Returns summary.',
input_schema: { type: 'object', properties: { task: { type: 'string' } }, required: ['task'] },
};
async function execute(toolUse) {
if (toolUse.name === 'research_subagent') {
return await runSubAgent(toolUse.input.task);
}
}
→ Tool 가 sub-LLM 호출.
결과 형식
// 텍스트
return { content: 'Result text' };
// JSON (LLM 가 parse)
return { content: JSON.stringify({ count: 5, items: [...] }) };
// 큰 결과 → reference
const fileId = await saveResult(big);
return { content: `Saved to file ${fileId}. Use read_file to access.` };
Tool 의 idempotency
동작 가 idempotent = 안전하게 retry.
GET (read): idempotent.
POST (create): 안 됨 — 중복.
DELETE: idempotent (이미 없음 = 같음).
→ Idempotency key:
"create_user with id 'abc'" 가 두 번 호출 = 1 user 만.
Streaming tool result
// 큰 결과 = streaming
async function* streamSearch(query) {
for (const doc of await search(query)) {
yield { type: 'text', text: JSON.stringify(doc) };
}
}
→ LLM 가 chunk 별 처리.
Composition: Output → Input
// Schema-aware 변환
const userResult = await tools.find_user('Alice');
// → { id: 'u1', email: 'alice@x.com' }
const emailResult = await tools.send_email({
to: userResult.email, // 직접 전달
body: '...'
});
→ LLM 가 자연 — 하지만 schema 일치 검증.
Eval
// Tool selection eval
const cases = [
{ input: 'Email Alice the report', expected: ['find_user', 'get_report', 'send_email'] },
{ input: 'Whats 2+2', expected: [] }, // tool 호출 X
];
for (const c of cases) {
const r = await agent.run(c.input);
const tools = r.tool_calls.map(t => t.name);
assert(deepEqual(tools, c.expected));
}
Caching tool results
const cache = new Map();
async function cachedTool(name, input) {
const key = `${name}:${JSON.stringify(input)}`;
if (cache.has(key)) return cache.get(key);
const r = await tools[name](input);
cache.set(key, r);
return r;
}
→ 같은 tool + 같은 input = 1번만.
Tool naming pitfall
❌ search_v2 (옛 search 와 혼동)
❌ doSomething (모호)
❌ user_handler (handler X)
✅ list_users
✅ create_order
✅ delete_file (destructive 명확)
Production debugging
Tool log:
- name + input + output
- Latency
- Error
- LLM 가 호출 한 reasoning
→ Bad output = tool error 인지 LLM error 인지.
🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| Simple tool | Inline schema |
| Many tools | MCP server |
| Destructive | Confirmation |
| Big result | File / streaming |
| Parallel | Promise.all in agent loop |
| Permission | Whitelist + path filter |
| Sub-task | Sub-agent tool |
| Tracing | LangSmith / Langfuse |
❌ 안티패턴
- Tool 너무 많음: context 폭발, LLM 헷갈림.
- Schema 모호: LLM 가 잘못 호출.
- Permissions 없음: rm -rf 가능.
- Error 가 string only: LLM 가 retry 못 함.
- No idempotency: 중복 effect.
- Tool 가 큰 output: token 폭발.
- Confirmation 안 함 (destructive): 사고.
🤖 LLM 활용 힌트
- Tool name + description 가 가장 큰 lever.
- MCP 가 표준 — Claude / Cursor 가 native.
- Permission whitelist 필수.
- ReAct loop 가 default agent 패턴.