--- id: ai-tool-composition-deep title: Tool Composition — agent 가 tool 사용 / chain category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ai, agents, tools, vibe-coding] tech_stack: { language: "TS / Python", applicable_to: ["AI"] } applied_in: [] aliases: [tool composition, function calling, tool use, ReAct, MCP, agent tools] --- # 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) ```ts 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: 명확. ``` ```ts // ❌ 모호 { 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 ```ts // 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 ```ts // 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 ```ts 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. ### 권한 / 안전 ```ts 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) ```ts 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) ```ts // 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 ```ts class ToolRegistry { private tools = new Map(); 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) ```ts 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 호출. ### 결과 형식 ```ts // 텍스트 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 ```ts // 큰 결과 = streaming async function* streamSearch(query) { for (const doc of await search(query)) { yield { type: 'text', text: JSON.stringify(doc) }; } } ``` → LLM 가 chunk 별 처리. ### Composition: Output → Input ```ts // 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 ```ts // 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 ```ts 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 패턴. ## 🔗 관련 문서 - [[AI_Function_Calling_Deep]] - [[AI_MCP_Server_Building]] - [[AI_Multi_Agent_Coordination]]