viruagent 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -2
- package/config/agent-prompt.md +31 -0
- package/docs/agent-pattern-guide.md +574 -0
- package/docs/hybrid-db-agent-guide.md +484 -0
- package/package.json +1 -1
- package/src/agent.js +56 -8
- package/src/lib/ai.js +281 -1
package/README.md
CHANGED
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
## 뭘 할 수 있나
|
|
16
16
|
|
|
17
|
-
-
|
|
17
|
+
- **자연어로 글 발행** — "AI 트렌드로 글 써서 발행해줘"라고 말하면 AI가 알아서 글 생성부터 발행까지 처리합니다.
|
|
18
|
+
- **에이전트 패턴** — OpenAI Function Calling 기반. AI가 도구를 자율적으로 선택하고 실행하는 에이전트 루프로 동작합니다.
|
|
19
|
+
- **수동 명령어 호환** — `/write`, `/edit`, `/publish` 같은 슬래시 명령어도 그대로 사용할 수 있습니다.
|
|
18
20
|
- **Unsplash 이미지 자동 삽입** — 글 생성 시 주제에 맞는 이미지를 자동 검색하고 티스토리에 업로드합니다. 첫 번째 이미지가 썸네일로 설정됩니다.
|
|
19
21
|
- **브라우저 로그인으로 세션 관리** — OAuth 설정 없이 Playwright로 실제 로그인해서 쿠키를 가져옵니다.
|
|
20
22
|
- **유연한 글 구조** — 5가지 글 유형(튜토리얼, 비교/리뷰, 리스트, 정보 가이드, 인사이트)을 AI가 주제에 맞게 자율 선택합니다.
|
|
21
23
|
- **CLI UX** — 커맨드 힌트, 화살표 키 메뉴, 상태바 등을 넣어서 터미널에서도 불편하지 않게 만들었습니다.
|
|
22
|
-
- **HTML 유지하면서 수정** — 글 구조를 깨뜨리지 않고 특정 부분만 고치거나 말투를 바꿀 수 있습니다.
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -67,6 +68,44 @@ Unsplash API Key는 `/set api` 명령어로 나중에 설정할 수 있습니다
|
|
|
67
68
|
|
|
68
69
|
---
|
|
69
70
|
|
|
71
|
+
## 에이전트 모드
|
|
72
|
+
|
|
73
|
+
슬래시 명령어 없이 자연어로 말하면 AI가 자율적으로 동작합니다.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
viruagent> AI 에이전트 주제로 글 써서 발행해줘
|
|
77
|
+
|
|
78
|
+
⠹
|
|
79
|
+
✓ 글 생성 완료: "AI 에이전트 완벽 가이드: 챗봇과의 7가지 차이"
|
|
80
|
+
⠧
|
|
81
|
+
✓ 발행 완료! https://tkman.tistory.com/60
|
|
82
|
+
|
|
83
|
+
AI
|
|
84
|
+
블로그 글이 성공적으로 발행되었습니다!
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 자연어 요청 예시
|
|
88
|
+
|
|
89
|
+
| 입력 | AI가 하는 일 |
|
|
90
|
+
|------|-------------|
|
|
91
|
+
| "AI 트렌드로 글 써줘" | `generate_post` 호출 → 초안 생성 |
|
|
92
|
+
| "서론을 더 흥미롭게 수정해줘" | `edit_post` 호출 → 초안 수정 |
|
|
93
|
+
| "발행해줘" | `publish_post` 호출 → 즉시 발행 |
|
|
94
|
+
| "비공개로 발행해줘" | 공개설정 변경 후 발행 |
|
|
95
|
+
| "글 써서 바로 발행해줘" | 생성 → 발행 연속 실행 |
|
|
96
|
+
|
|
97
|
+
### 동작 원리
|
|
98
|
+
|
|
99
|
+
1. 사용자가 자연어로 요청
|
|
100
|
+
2. OpenAI Function Calling으로 AI가 적절한 도구 선택
|
|
101
|
+
3. 코드가 도구 실행 → 결과를 AI에게 전달
|
|
102
|
+
4. AI가 다음 행동 판단 (추가 도구 호출 또는 텍스트 응답)
|
|
103
|
+
5. 모든 작업 완료 후 결과 보고
|
|
104
|
+
|
|
105
|
+
기존 `/write`, `/edit`, `/publish` 명령어도 100% 호환됩니다.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
70
109
|
## Claude Code로 글쓰기
|
|
71
110
|
|
|
72
111
|
[Claude Code](https://claude.com/claude-code)에서 자연어로 명령할 수 있습니다.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
당신은 티스토리 블로그 글쓰기를 돕는 자율형 AI 에이전트입니다.
|
|
2
|
+
사용자의 자연어 요청을 이해하고, 적절한 도구를 순서대로 호출하여 블로그 글 작성부터 발행까지 자율적으로 수행합니다.
|
|
3
|
+
|
|
4
|
+
## 핵심 원칙: 요청받은 작업은 끝까지 실행하라
|
|
5
|
+
|
|
6
|
+
사용자가 여러 작업을 한 문장에 요청하면 (예: "글 써서 발행해줘"), **중간에 멈추지 말고** 모든 도구를 순서대로 호출하여 완료하세요.
|
|
7
|
+
|
|
8
|
+
- "글 써서 발행해줘" → `generate_post` 호출 → 바로 `publish_post` 호출
|
|
9
|
+
- "글 써서 수정하고 발행해줘" → `generate_post` → `edit_post` → `publish_post`
|
|
10
|
+
- 중간에 "다음 뭘 할까요?" 같은 질문을 하지 마세요.
|
|
11
|
+
- 모든 작업이 끝난 후 결과를 한 번에 보고하세요.
|
|
12
|
+
|
|
13
|
+
## 도구 사용 원칙
|
|
14
|
+
|
|
15
|
+
1. 사용자가 글 작성을 요청하면 `generate_post`를 호출하세요.
|
|
16
|
+
2. 수정 요청이면 `edit_post`를 호출하세요.
|
|
17
|
+
3. 미리보기 요청이면 `preview_post`를 호출하세요.
|
|
18
|
+
4. 발행 요청이면 바로 `publish_post`를 호출하세요. 별도 확인을 구하지 마세요.
|
|
19
|
+
5. 카테고리나 공개설정 변경 요청이면 `set_category` 또는 `set_visibility`를 호출하세요.
|
|
20
|
+
6. 현재 상태를 파악해야 할 때 `get_blog_status`를 호출하세요.
|
|
21
|
+
|
|
22
|
+
## 대화 스타일
|
|
23
|
+
|
|
24
|
+
- 한국어로 대화하세요.
|
|
25
|
+
- 도구 호출 결과를 사용자에게 간결하게 요약해서 전달하세요.
|
|
26
|
+
- 작업만 요청받았을 때는 선택지를 제시하지 마세요. 바로 실행하세요.
|
|
27
|
+
- 불필요한 도구 호출을 하지 마세요. 단순 대화는 도구 없이 응답하세요.
|
|
28
|
+
|
|
29
|
+
## 주의사항
|
|
30
|
+
|
|
31
|
+
- 초안이 없는 상태에서 수정/미리보기/발행을 요청하면 먼저 글을 작성하라고 안내하세요.
|
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
# AI 에이전트 패턴 & Function Calling 학습 가이드
|
|
2
|
+
|
|
3
|
+
## 목차
|
|
4
|
+
|
|
5
|
+
1. [에이전트란 무엇인가](#1-에이전트란-무엇인가)
|
|
6
|
+
2. [기존 방식 vs 에이전트 방식](#2-기존-방식-vs-에이전트-방식)
|
|
7
|
+
3. [Function Calling 핵심 원리](#3-function-calling-핵심-원리)
|
|
8
|
+
4. [에이전트 루프 구조](#4-에이전트-루프-구조)
|
|
9
|
+
5. [Tool 정의 작성법](#5-tool-정의-작성법)
|
|
10
|
+
6. [Tool Executor 패턴](#6-tool-executor-패턴)
|
|
11
|
+
7. [실전 흐름 추적](#7-실전-흐름-추적)
|
|
12
|
+
8. [시스템 프롬프트의 역할](#8-시스템-프롬프트의-역할)
|
|
13
|
+
9. [토큰 비용 구조](#9-토큰-비용-구조)
|
|
14
|
+
10. [안전장치 설계](#10-안전장치-설계)
|
|
15
|
+
11. [확장 패턴](#11-확장-패턴)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. 에이전트란 무엇인가
|
|
20
|
+
|
|
21
|
+
### 일반 챗봇
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
사용자 → 질문 → AI → 텍스트 답변
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
AI는 **말만** 할 수 있다. 실제 행동(파일 저장, API 호출, DB 쿼리)은 불가능.
|
|
28
|
+
|
|
29
|
+
### 에이전트
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
사용자 → 요청 → AI → "이 도구를 이렇게 써야겠다" → 코드가 실행 → 결과를 AI에게 → AI가 다음 판단
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
AI가 **행동을 선택**하고, 코드가 **실행**하고, 결과를 보고 AI가 **다음 행동을 판단**한다.
|
|
36
|
+
|
|
37
|
+
### 핵심 차이
|
|
38
|
+
|
|
39
|
+
| | 챗봇 | 에이전트 |
|
|
40
|
+
| --------- | ------------- | -------------------- |
|
|
41
|
+
| AI의 역할 | 텍스트 생성 | **판단 + 도구 선택** |
|
|
42
|
+
| 실행 주체 | 없음 | 코드(Tool Executor) |
|
|
43
|
+
| 루프 | 1회 요청-응답 | **반복 루프** |
|
|
44
|
+
| 자율성 | 없음 | AI가 다음 행동 결정 |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. 기존 방식 vs 에이전트 방식
|
|
49
|
+
|
|
50
|
+
### 기존: 사용자가 라우터
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
사용자가 "/write AI 트렌드" 입력
|
|
54
|
+
→ 코드가 "write" 명령어 파싱
|
|
55
|
+
→ generatePost("AI 트렌드") 직접 호출
|
|
56
|
+
→ 결과 출력
|
|
57
|
+
|
|
58
|
+
사용자가 "/edit 서론 수정" 입력
|
|
59
|
+
→ 코드가 "edit" 명령어 파싱
|
|
60
|
+
→ revisePost(content, "서론 수정") 직접 호출
|
|
61
|
+
→ 결과 출력
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**사용자**가 어떤 함수를 쓸지 결정한다. 코드는 `if/switch`로 매핑만 한다.
|
|
65
|
+
|
|
66
|
+
### 에이전트: AI가 라우터
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
사용자가 "AI 트렌드로 글 써서 발행해줘" 입력
|
|
70
|
+
→ AI가 판단: "generate_post를 먼저 호출해야겠다"
|
|
71
|
+
→ 코드가 generatePost() 실행 → 결과를 AI에게 전달
|
|
72
|
+
→ AI가 판단: "글이 완성됐으니 발행 확인을 물어봐야겠다"
|
|
73
|
+
→ "발행할까요?" 텍스트 응답
|
|
74
|
+
→ 사용자: "응"
|
|
75
|
+
→ AI가 판단: "publish_post 호출"
|
|
76
|
+
→ 코드가 publishPost() 실행
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**AI**가 어떤 함수를 쓸지 결정한다. 코드는 AI의 결정을 실행만 한다.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Function Calling 핵심 원리
|
|
84
|
+
|
|
85
|
+
OpenAI의 Function Calling은 AI에게 **"너는 이런 도구를 쓸 수 있어"**라고 알려주는 메커니즘이다.
|
|
86
|
+
|
|
87
|
+
### API 요청 구조
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const response = await openai.chat.completions.create({
|
|
91
|
+
model: 'gpt-4o-mini',
|
|
92
|
+
messages: [
|
|
93
|
+
{ role: 'system', content: '당신은 블로그 에이전트입니다.' },
|
|
94
|
+
{ role: 'user', content: 'AI 트렌드로 글 써줘' },
|
|
95
|
+
],
|
|
96
|
+
tools: [
|
|
97
|
+
// ← 여기서 도구를 알려줌
|
|
98
|
+
{
|
|
99
|
+
type: 'function',
|
|
100
|
+
function: {
|
|
101
|
+
name: 'generate_post',
|
|
102
|
+
description: '블로그 글 초안을 생성합니다.',
|
|
103
|
+
parameters: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
topic: { type: 'string', description: '글 주제' },
|
|
107
|
+
},
|
|
108
|
+
required: ['topic'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### API 응답 — 두 가지 경우
|
|
117
|
+
|
|
118
|
+
**경우 1: 도구를 호출하겠다고 판단**
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"choices": [
|
|
123
|
+
{
|
|
124
|
+
"message": {
|
|
125
|
+
"role": "assistant",
|
|
126
|
+
"content": null,
|
|
127
|
+
"tool_calls": [
|
|
128
|
+
{
|
|
129
|
+
"id": "call_abc123",
|
|
130
|
+
"type": "function",
|
|
131
|
+
"function": {
|
|
132
|
+
"name": "generate_post",
|
|
133
|
+
"arguments": "{\"topic\": \"AI 트렌드\"}"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
AI가 직접 함수를 실행하는 게 **아니다**. "이 함수를 이 인자로 호출해줘"라고 **요청**하는 것이다.
|
|
144
|
+
|
|
145
|
+
**경우 2: 도구 없이 텍스트로 답변**
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"choices": [
|
|
150
|
+
{
|
|
151
|
+
"message": {
|
|
152
|
+
"role": "assistant",
|
|
153
|
+
"content": "글이 생성되었습니다! 제목은 'AI 트렌드 2026'입니다."
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`tool_calls`가 없으면 일반 텍스트 응답. 이게 루프의 종료 조건이 된다.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 4. 에이전트 루프 구조
|
|
165
|
+
|
|
166
|
+
에이전트의 핵심은 **while 루프**다.
|
|
167
|
+
|
|
168
|
+
### 의사코드
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
messages = [시스템 프롬프트, 사용자 메시지]
|
|
172
|
+
|
|
173
|
+
반복 (최대 10회):
|
|
174
|
+
응답 = OpenAI API 호출(messages, tools)
|
|
175
|
+
|
|
176
|
+
if 응답에 tool_calls가 있으면:
|
|
177
|
+
각 tool_call에 대해:
|
|
178
|
+
함수 실행
|
|
179
|
+
결과를 messages에 추가
|
|
180
|
+
→ 루프 계속 (다시 API 호출)
|
|
181
|
+
|
|
182
|
+
else:
|
|
183
|
+
텍스트 응답을 사용자에게 표시
|
|
184
|
+
→ 루프 종료
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 실제 코드 (viruagent의 runAgent)
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
for (let loop = 0; loop < MAX_LOOPS; loop++) {
|
|
191
|
+
// 1. API 호출
|
|
192
|
+
const res = await openai.chat.completions.create({
|
|
193
|
+
model,
|
|
194
|
+
messages,
|
|
195
|
+
tools: agentTools,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const msg = res.choices[0].message;
|
|
199
|
+
messages.push(msg); // AI 응답을 히스토리에 추가
|
|
200
|
+
|
|
201
|
+
// 2. 분기: tool_calls 유무
|
|
202
|
+
if (!msg.tool_calls || msg.tool_calls.length === 0) {
|
|
203
|
+
// 텍스트 응답 → 루프 종료
|
|
204
|
+
return msg.content;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 3. 도구 실행 → 결과를 messages에 추가
|
|
208
|
+
for (const toolCall of msg.tool_calls) {
|
|
209
|
+
const result = await executeAgentTool(toolCall.function.name, args);
|
|
210
|
+
messages.push({
|
|
211
|
+
role: 'tool', // ← 특수 role
|
|
212
|
+
tool_call_id: toolCall.id, // ← 어떤 호출의 결과인지 매핑
|
|
213
|
+
content: JSON.stringify(result),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// → 루프 처음으로 돌아가서 다시 API 호출
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### messages 배열의 흐름
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
[1] { role: "system", content: "에이전트 프롬프트" }
|
|
224
|
+
[2] { role: "user", content: "AI 트렌드로 글 써줘" }
|
|
225
|
+
↓ API 호출
|
|
226
|
+
[3] { role: "assistant", tool_calls: [generate_post] } ← AI 판단
|
|
227
|
+
[4] { role: "tool", content: '{"title":"AI 트렌드"}' } ← 실행 결과
|
|
228
|
+
↓ API 재호출 ([1]~[4] 전체 전송)
|
|
229
|
+
[5] { role: "assistant", content: "글이 완성되었습니다!" } ← 최종 응답
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**매 루프마다 전체 messages를 다시 보낸다.** AI는 이전 맥락을 모두 보고 다음 판단을 한다.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 5. Tool 정의 작성법
|
|
237
|
+
|
|
238
|
+
### 기본 구조
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
{
|
|
242
|
+
type: "function",
|
|
243
|
+
function: {
|
|
244
|
+
name: "함수이름", // AI가 호출할 때 사용하는 이름
|
|
245
|
+
description: "이 도구가 뭘 하는지", // AI가 판단 근거로 사용
|
|
246
|
+
parameters: { // JSON Schema 형식
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
param1: {
|
|
250
|
+
type: "string",
|
|
251
|
+
description: "파라미터 설명" // AI가 값을 채울 때 참고
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
required: ["param1"]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 좋은 description 작성법
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
❌ "글을 만든다" → 너무 모호
|
|
264
|
+
✅ "블로그 글 초안을 생성합니다." → 명확한 행동
|
|
265
|
+
✅ "현재 초안을 블로그에 발행합니다. → 행동 + 대상
|
|
266
|
+
반드시 사용자 확인 후 호출하세요." → 조건까지 명시
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
description은 AI의 **판단 근거**가 된다. 잘 쓰면 AI가 적절한 타이밍에 적절한 도구를 선택한다.
|
|
270
|
+
|
|
271
|
+
### 파라미터 없는 도구
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
{
|
|
275
|
+
name: "preview_post",
|
|
276
|
+
description: "현재 초안을 미리봅니다.",
|
|
277
|
+
parameters: { type: "object", properties: {} } // 빈 객체
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 선택적 파라미터
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
{
|
|
285
|
+
name: "generate_post",
|
|
286
|
+
parameters: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
topic: { type: "string", description: "글 주제" },
|
|
290
|
+
tone: { type: "string", description: "글 톤 (선택)" }
|
|
291
|
+
},
|
|
292
|
+
required: ["topic"] // tone은 required에 없음 → 선택
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## 6. Tool Executor 패턴
|
|
300
|
+
|
|
301
|
+
AI가 "generate_post를 호출해줘"라고 하면, 실제로 실행하는 코드가 필요하다.
|
|
302
|
+
|
|
303
|
+
### switch 패턴 (viruagent 방식)
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
const executeAgentTool = async (name, args, context) => {
|
|
307
|
+
switch (name) {
|
|
308
|
+
case 'generate_post': {
|
|
309
|
+
const result = await generatePost(args.topic, { tone: args.tone });
|
|
310
|
+
context.state.draft = result; // 상태 업데이트
|
|
311
|
+
return { success: true, title: result.title };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case 'edit_post': {
|
|
315
|
+
if (!context.state.draft) {
|
|
316
|
+
return { error: '초안이 없습니다.' }; // 에러도 JSON으로 반환
|
|
317
|
+
}
|
|
318
|
+
const result = await revisePost(context.state.draft.content, args.instruction);
|
|
319
|
+
context.state.draft = result;
|
|
320
|
+
return { success: true, title: result.title };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case 'publish_post': {
|
|
324
|
+
const result = await context.publishPost({ ... });
|
|
325
|
+
return { success: true, url: result.entryUrl };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
default:
|
|
329
|
+
return { error: `알 수 없는 도구: ${name}` };
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### 핵심 원칙
|
|
335
|
+
|
|
336
|
+
1. **결과는 항상 JSON 문자열로 반환** — AI가 읽을 수 있어야 함
|
|
337
|
+
2. **에러도 throw하지 않고 `{ error: "..." }`로 반환** — AI가 에러를 보고 대안을 판단할 수 있음
|
|
338
|
+
3. **상태 변경은 여기서** — `state.draft = result` 같은 부수효과 처리
|
|
339
|
+
|
|
340
|
+
### Map 패턴 (대안)
|
|
341
|
+
|
|
342
|
+
도구가 많아지면 switch 대신 Map으로 관리할 수도 있다:
|
|
343
|
+
|
|
344
|
+
```js
|
|
345
|
+
const toolHandlers = {
|
|
346
|
+
generate_post: async (args, ctx) => { ... },
|
|
347
|
+
edit_post: async (args, ctx) => { ... },
|
|
348
|
+
publish_post: async (args, ctx) => { ... },
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const executeAgentTool = async (name, args, ctx) => {
|
|
352
|
+
const handler = toolHandlers[name];
|
|
353
|
+
if (!handler) return { error: `알 수 없는 도구: ${name}` };
|
|
354
|
+
return handler(args, ctx);
|
|
355
|
+
};
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 7. 실전 흐름 추적
|
|
361
|
+
|
|
362
|
+
### 시나리오: "AI 트렌드로 글 써서 서론 수정하고 발행해줘"
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
[사용자 입력]
|
|
366
|
+
"AI 트렌드로 글 써서 서론 수정하고 발행해줘"
|
|
367
|
+
|
|
368
|
+
[루프 1] API 호출
|
|
369
|
+
→ AI 판단: 먼저 글을 생성해야 한다
|
|
370
|
+
→ tool_calls: [{ name: "generate_post", args: { topic: "AI 트렌드" } }]
|
|
371
|
+
→ 코드 실행: generatePost("AI 트렌드")
|
|
372
|
+
→ 결과: { success: true, title: "2026 AI 트렌드 총정리" }
|
|
373
|
+
|
|
374
|
+
[루프 2] API 재호출 (이전 맥락 + tool 결과 포함)
|
|
375
|
+
→ AI 판단: 이제 서론을 수정해야 한다
|
|
376
|
+
→ tool_calls: [{ name: "edit_post", args: { instruction: "서론을 더 흥미롭게" } }]
|
|
377
|
+
→ 코드 실행: revisePost(content, "서론을 더 흥미롭게")
|
|
378
|
+
→ 결과: { success: true, title: "2026 AI 트렌드 총정리" }
|
|
379
|
+
|
|
380
|
+
[루프 3] API 재호출
|
|
381
|
+
→ AI 판단: 발행 전에 사용자 확인이 필요하다 (프롬프트 규칙)
|
|
382
|
+
→ tool_calls 없음
|
|
383
|
+
→ 텍스트: "글이 준비되었습니다! 발행할까요?"
|
|
384
|
+
→ 루프 종료
|
|
385
|
+
|
|
386
|
+
[사용자 입력]
|
|
387
|
+
"응 발행해"
|
|
388
|
+
|
|
389
|
+
[루프 1] API 호출
|
|
390
|
+
→ AI 판단: 사용자가 승인했으니 발행
|
|
391
|
+
→ tool_calls: [{ name: "publish_post", args: {} }]
|
|
392
|
+
→ 코드 실행: publishPost(...)
|
|
393
|
+
→ 결과: { success: true, url: "https://blog.example.com/123" }
|
|
394
|
+
|
|
395
|
+
[루프 2] API 재호출
|
|
396
|
+
→ tool_calls 없음
|
|
397
|
+
→ 텍스트: "발행 완료! https://blog.example.com/123"
|
|
398
|
+
→ 루프 종료
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 8. 시스템 프롬프트의 역할
|
|
404
|
+
|
|
405
|
+
에이전트의 **행동 규칙**을 정하는 것이 시스템 프롬프트다.
|
|
406
|
+
|
|
407
|
+
### tool description vs 시스템 프롬프트
|
|
408
|
+
|
|
409
|
+
| | tool description | 시스템 프롬프트 |
|
|
410
|
+
| ---- | ------------------------ | --------------------------------------- |
|
|
411
|
+
| 위치 | tools 배열 안 | messages[0] |
|
|
412
|
+
| 역할 | "이 도구는 뭘 한다" | "너는 누구고, 어떻게 행동해라" |
|
|
413
|
+
| 예시 | "블로그 글을 생성합니다" | "발행 전에 반드시 사용자 확인을 구하라" |
|
|
414
|
+
|
|
415
|
+
### viruagent의 에이전트 프롬프트 구조
|
|
416
|
+
|
|
417
|
+
```markdown
|
|
418
|
+
# 역할 정의
|
|
419
|
+
|
|
420
|
+
당신은 티스토리 블로그 글쓰기를 돕는 자율형 AI 에이전트입니다.
|
|
421
|
+
|
|
422
|
+
# 도구 사용 원칙 (판단 기준)
|
|
423
|
+
|
|
424
|
+
1. 글 작성 요청 → generate_post
|
|
425
|
+
2. 수정 요청 → edit_post
|
|
426
|
+
3. 발행 요청 → 반드시 확인 먼저!
|
|
427
|
+
|
|
428
|
+
# 행동 제약
|
|
429
|
+
|
|
430
|
+
- 초안 없이 수정/발행 요청 → 안내
|
|
431
|
+
- 불필요한 도구 호출 금지
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
시스템 프롬프트가 없으면 AI는 **아무 도구나 마구 호출**할 수 있다. 프롬프트로 행동 경계를 잡아주는 것이 중요하다.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## 9. 토큰 비용 구조
|
|
439
|
+
|
|
440
|
+
### 매 루프마다 전송되는 토큰
|
|
441
|
+
|
|
442
|
+
```
|
|
443
|
+
시스템 프롬프트 ~300 토큰 (매번 고정)
|
|
444
|
+
tools 정의 (7개) ~800 토큰 (매번 고정)
|
|
445
|
+
대화 히스토리 누적 증가
|
|
446
|
+
tool 결과 누적 증가
|
|
447
|
+
──────────────────────────────
|
|
448
|
+
루프 1 입력: ~1,200 토큰
|
|
449
|
+
루프 2 입력: ~1,500 토큰 (이전 맥락 추가)
|
|
450
|
+
루프 3 입력: ~1,800 토큰
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### 비용 최적화 전략
|
|
454
|
+
|
|
455
|
+
| 전략 | 효과 |
|
|
456
|
+
| ------------------------------- | -------------------- |
|
|
457
|
+
| 히스토리 길이 제한 (최근 N개만) | 누적 비용 억제 |
|
|
458
|
+
| tool 결과를 요약해서 반환 | 컨텍스트 절약 |
|
|
459
|
+
| 불필요한 tool 제거 | tools 정의 토큰 감소 |
|
|
460
|
+
| 작은 모델 사용 (gpt-4o-mini) | 단가 자체를 낮춤 |
|
|
461
|
+
|
|
462
|
+
### 모델별 비용 비교 (루프 3회 기준, ~5,000 입력 토큰)
|
|
463
|
+
|
|
464
|
+
| 모델 | 입력 단가 | 예상 비용 |
|
|
465
|
+
| ----------- | --------- | -------------- |
|
|
466
|
+
| gpt-4o-mini | $0.15/1M | ~$0.001 (1원) |
|
|
467
|
+
| gpt-4o | $2.5/1M | ~$0.013 (17원) |
|
|
468
|
+
| gpt-4.1 | $2.0/1M | ~$0.010 (13원) |
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## 10. 안전장치 설계
|
|
473
|
+
|
|
474
|
+
### 무한 루프 방지
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
const MAX_LOOPS = 10;
|
|
478
|
+
for (let loop = 0; loop < MAX_LOOPS; loop++) {
|
|
479
|
+
// ... 루프 초과 시 강제 종료
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
AI가 도구를 호출하고 → 결과를 보고 → 또 호출하고... 무한히 반복할 수 있으므로 **반드시 상한**을 둔다.
|
|
484
|
+
|
|
485
|
+
### 위험한 동작 보호
|
|
486
|
+
|
|
487
|
+
```markdown
|
|
488
|
+
# 시스템 프롬프트에서
|
|
489
|
+
|
|
490
|
+
발행 요청 시에는 반드시 사용자에게 먼저 확인을 구하세요.
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
// tool description에서
|
|
495
|
+
{
|
|
496
|
+
name: "publish_post",
|
|
497
|
+
description: "현재 초안을 블로그에 발행합니다. 반드시 사용자 확인 후 호출하세요."
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**이중 안전장치**: 시스템 프롬프트 + tool description 양쪽에서 제약.
|
|
502
|
+
|
|
503
|
+
### 에러 격리
|
|
504
|
+
|
|
505
|
+
```js
|
|
506
|
+
let result;
|
|
507
|
+
try {
|
|
508
|
+
result = await executeAgentTool(name, args, context);
|
|
509
|
+
} catch (e) {
|
|
510
|
+
result = { error: e.message }; // throw하지 않고 에러를 AI에게 전달
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
도구 실행 실패 시 루프가 죽지 않고, AI가 에러를 보고 대안을 판단할 수 있다.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## 11. 확장 패턴
|
|
519
|
+
|
|
520
|
+
### 병렬 도구 호출 (Parallel Tool Calls)
|
|
521
|
+
|
|
522
|
+
AI가 한 번에 여러 도구를 호출할 수 있다:
|
|
523
|
+
|
|
524
|
+
```json
|
|
525
|
+
{
|
|
526
|
+
"tool_calls": [{ "name": "get_blog_status" }, { "name": "generate_post", "args": { "topic": "AI" } }]
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
viruagent는 이미 `for...of`로 순차 처리하지만, `Promise.all`로 병렬 실행도 가능:
|
|
531
|
+
|
|
532
|
+
```js
|
|
533
|
+
const results = await Promise.all(msg.tool_calls.map((tc) => executeAgentTool(tc.function.name, args)));
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Structured Outputs (응답 형식 강제)
|
|
537
|
+
|
|
538
|
+
```js
|
|
539
|
+
response_format: {
|
|
540
|
+
type: 'json_object';
|
|
541
|
+
} // JSON 응답 강제
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
도구 결과뿐 아니라 AI의 최종 응답도 구조화할 수 있다.
|
|
545
|
+
|
|
546
|
+
### Multi-Agent (다중 에이전트)
|
|
547
|
+
|
|
548
|
+
```
|
|
549
|
+
[매니저 에이전트]
|
|
550
|
+
├── [글쓰기 에이전트] → generate_post, edit_post
|
|
551
|
+
├── [SEO 에이전트] → analyze_seo, suggest_keywords
|
|
552
|
+
└── [발행 에이전트] → publish_post, schedule_post
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
각 에이전트가 자기 전문 도구만 갖고, 매니저가 작업을 분배하는 패턴.
|
|
556
|
+
|
|
557
|
+
### 메모리 (장기 기억)
|
|
558
|
+
|
|
559
|
+
```js
|
|
560
|
+
tools: [
|
|
561
|
+
{ name: 'save_memory', description: '중요 정보를 저장합니다' },
|
|
562
|
+
{ name: 'search_memory', description: '이전에 저장한 정보를 검색합니다' },
|
|
563
|
+
];
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
에이전트에게 기억 저장/검색 도구를 주면 대화 세션을 넘어서 정보를 유지할 수 있다.
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## 참고 자료
|
|
571
|
+
|
|
572
|
+
- [OpenAI Function Calling 공식 문서](https://platform.openai.com/docs/guides/function-calling)
|
|
573
|
+
- [OpenAI API Reference - Chat Completions](https://platform.openai.com/docs/api-reference/chat/create)
|
|
574
|
+
- [Building AI Agents (OpenAI Cookbook)](https://cookbook.openai.com/examples/how_to_call_functions_with_chat_models)
|