[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
---
|
||||
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<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)
|
||||
```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]]
|
||||
Reference in New Issue
Block a user