docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
---
|
||||
id: ai-structured-output-zod
|
||||
title: LLM Structured Output — Zod / Function Calling
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [ai, llm, structured, zod, vibe-coding]
|
||||
tech_stack: { language: "TS / Zod / OpenAI / Anthropic", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [structured output, JSON mode, function calling, tool use, response_format]
|
||||
---
|
||||
|
||||
# LLM Structured Output
|
||||
|
||||
> JSON 강제 prompt 만으로는 신뢰 X. **OpenAI `response_format: { type: 'json_schema' }` / Anthropic tool_use** 가 schema 보장. **Zod → JSON Schema** 가 표준 워크플로.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- JSON mode: 어떤 JSON 도 통과 (schema 미보장).
|
||||
- Structured output (OpenAI): JSON Schema 기반 **enforce**. parse 실패 0%.
|
||||
- Tool use (Anthropic): 함수 호출 형태 — input_schema 강제.
|
||||
- Zod → JSON Schema: `zod-to-json-schema` 라이브러리.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### OpenAI structured output
|
||||
```ts
|
||||
import OpenAI from 'openai';
|
||||
import { zodResponseFormat } from 'openai/helpers/zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
const Recipe = z.object({
|
||||
title: z.string(),
|
||||
servings: z.number().int().positive(),
|
||||
ingredients: z.array(z.object({
|
||||
name: z.string(),
|
||||
qty: z.number(),
|
||||
unit: z.enum(['g', 'ml', 'cup', 'tsp']),
|
||||
})),
|
||||
steps: z.array(z.string()).max(10),
|
||||
});
|
||||
|
||||
const client = new OpenAI();
|
||||
const r = await client.beta.chat.completions.parse({
|
||||
model: 'gpt-4o',
|
||||
messages: [{ role: 'user', content: 'Spaghetti carbonara recipe' }],
|
||||
response_format: zodResponseFormat(Recipe, 'recipe'),
|
||||
});
|
||||
|
||||
const recipe = r.choices[0].message.parsed!; // 타입 = z.infer<typeof Recipe>
|
||||
```
|
||||
|
||||
### Anthropic tool use (structured)
|
||||
```ts
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
const client = new Anthropic();
|
||||
const r = await client.messages.create({
|
||||
model: 'claude-opus-4-7',
|
||||
max_tokens: 1024,
|
||||
tools: [{
|
||||
name: 'extract_recipe',
|
||||
description: 'Extract recipe data',
|
||||
input_schema: zodToJsonSchema(Recipe) as Anthropic.Messages.Tool.InputSchema,
|
||||
}],
|
||||
tool_choice: { type: 'tool', name: 'extract_recipe' }, // 강제
|
||||
messages: [{ role: 'user', content: 'Spaghetti carbonara recipe' }],
|
||||
});
|
||||
|
||||
const block = r.content.find(b => b.type === 'tool_use');
|
||||
const recipe = Recipe.parse(block!.input);
|
||||
```
|
||||
|
||||
### 검증 + 재시도
|
||||
```ts
|
||||
async function getRecipe(query: string, attempts = 3): Promise<Recipe> {
|
||||
let lastErr: unknown;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
const raw = await callLLM(query);
|
||||
return Recipe.parse(raw); // throws ZodError on fail
|
||||
} catch (e) {
|
||||
lastErr = e;
|
||||
// 재시도 시 에러 메시지를 LLM 에 피드백
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
```
|
||||
|
||||
### 점진적 schema (간단 → 복잡)
|
||||
```ts
|
||||
// V1: 단순
|
||||
const SimpleRecipe = z.object({ title: z.string(), steps: z.array(z.string()) });
|
||||
// 동작 확인
|
||||
|
||||
// V2: 더 정밀
|
||||
const Recipe = SimpleRecipe.extend({
|
||||
servings: z.number().int().positive(),
|
||||
ingredients: z.array(IngredientSchema),
|
||||
});
|
||||
```
|
||||
|
||||
### Discriminated union (여러 종류)
|
||||
```ts
|
||||
const Action = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('search'), query: z.string() }),
|
||||
z.object({ type: z.literal('calc'), expr: z.string() }),
|
||||
z.object({ type: z.literal('done'), answer: z.string() }),
|
||||
]);
|
||||
```
|
||||
|
||||
### Streaming + structured (OpenAI)
|
||||
```ts
|
||||
const stream = await client.beta.chat.completions.stream({
|
||||
model: 'gpt-4o',
|
||||
messages: [...],
|
||||
response_format: zodResponseFormat(Recipe, 'recipe'),
|
||||
});
|
||||
|
||||
for await (const ev of stream) {
|
||||
if (ev.event === 'content.delta') console.log(ev.parsed); // 부분 객체
|
||||
}
|
||||
|
||||
const final = (await stream.finalChatCompletion()).choices[0].message.parsed;
|
||||
```
|
||||
|
||||
### Function calling (legacy)
|
||||
```ts
|
||||
const r = await client.chat.completions.create({
|
||||
model: 'gpt-4o',
|
||||
messages: [...],
|
||||
tools: [{ type: 'function', function: { name: 'extract', parameters: zodToJsonSchema(Recipe) } }],
|
||||
tool_choice: { type: 'function', function: { name: 'extract' } },
|
||||
});
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| OpenAI 정확한 schema | Structured output |
|
||||
| Anthropic | Tool use + force tool |
|
||||
| 간단 JSON | JSON mode + Zod parse |
|
||||
| 여러 종류 액션 | Discriminated union |
|
||||
| Streaming partial | OpenAI stream + structured |
|
||||
| Schema 변동 | runtime parse + 재시도 |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **JSON 그대로 신뢰**: 자유 형식이면 누락 / 추가 키.
|
||||
- **Schema 거대**: enum 100개 / 50 필드 — LLM 도 정확히 못 채움.
|
||||
- **Optional 모두**: 강제 없으면 LLM 이 빠뜨림.
|
||||
- **Description 없음**: schema 만 있으면 LLM 이 의미 모름.
|
||||
- **Re-try infinite**: 3번 후 fallback 또는 사용자에게.
|
||||
- **Tool name 동사 X 명사 O**: tool_use 는 동사 권장 (`extract_recipe`).
|
||||
- **PII strict 검증 없음**: 잘못된 형식 통과.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Zod schema → zodResponseFormat (OpenAI) / zodToJsonSchema (Anthropic).
|
||||
- 강제 tool_choice 또는 response_format 으로 보장.
|
||||
- Description 풍부하게.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[AI_Prompt_Engineering_Patterns]]
|
||||
- [[AI_Streaming_LLM_Response]]
|
||||
Reference in New Issue
Block a user