๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ

Sonamu ๊ฒ€์ƒ‰ API์˜ ๋ฌธ์ œ

Sonamu๋กœ ์ง€์‹ ๋ฒ ์ด์Šค ์•ฑ์„ ๋งŒ๋“ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:
class DocumentModelClass extends BaseModel {
  @api({ httpMethod: 'POST' })
  async search(query: string) {
    // ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰
    const results = await this.findMany({
      wq: [
        ['title', 'LIKE', `%${query}%`],
        ['content', 'LIKE', `%${query}%`],
      ],
    });
    return results;
  }
}
์‚ฌ์šฉ์ž๊ฐ€ โ€œTypeScript ํ”„๋ ˆ์ž„์›Œํฌโ€๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด?
  • โœ… โ€œSonamu๋Š” TypeScript ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹คโ€ โ†’ ์ฐพ์Œ
  • โŒ โ€œSonamu๋Š” Node.js API ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹คโ€ โ†’ ๋ชป ์ฐพ์Œ (ํ‚ค์›Œ๋“œ ๋‹ค๋ฆ„)
  • โŒ โ€œSonamu is a TS frameworkโ€ โ†’ ๋ชป ์ฐพ์Œ (์˜์–ด)
  • โŒ โ€œํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ํ”„๋ž˜์ž„์›Œํฌโ€ โ†’ ๋ชป ์ฐพ์Œ (์˜คํƒ€)
ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰์˜ ํ•œ๊ณ„:
  • ๋™์˜์–ด ์ฒ˜๋ฆฌ ์•ˆ ๋จ
  • ํ‘œํ˜„ ๋ฐฉ์‹์ด ๋‹ฌ๋ผ์ง€๋ฉด ๋ชป ์ฐพ์Œ
  • ์˜คํƒ€์— ์ทจ์•ฝ
  • ์˜๋ฏธ๋Š” ๊ฐ™์€๋ฐ ๋‹จ์–ด๊ฐ€ ๋‹ค๋ฅด๋ฉด ์‹คํŒจ

์˜๋ฏธ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค

โ€œTypeScript ํ”„๋ ˆ์ž„์›Œํฌโ€์™€ โ€œNode.js API ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌโ€๋Š” ์˜๋ฏธ๊ฐ€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ํ‚ค์›Œ๋“œ๋Š” ๋‹ฌ๋ผ๋„ ๋ง์ด์ฃ . ์ด๋Ÿฐ ์˜๋ฏธ๋ฅผ ์ปดํ“จํ„ฐ๊ฐ€ ์ดํ•ดํ•˜๋ ค๋ฉด? โ†’ ์ž„๋ฒ ๋”ฉ(Embedding)

์ž„๋ฒ ๋”ฉ์ด๋ž€?

์ž„๋ฒ ๋”ฉ์€ ํ…์ŠคํŠธ๋ฅผ ๊ณ ์ฐจ์› ์ˆซ์ž ๋ฐฐ์—ด(๋ฒกํ„ฐ)๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค. ์˜๋ฏธ๊ฐ€ ์œ ์‚ฌํ•œ ํ…์ŠคํŠธ๋Š” ๋ฒกํ„ฐ ๊ณต๊ฐ„์—์„œ ๊ฐ€๊นŒ์šด ์œ„์น˜์— ๋ฐฐ์น˜๋ฉ๋‹ˆ๋‹ค. ํ•ต์‹ฌ:
  • ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ ๊ฐ€๋Šฅ
  • ๊ฐ€๊นŒ์šด ๋ฒกํ„ฐ = ์˜๋ฏธ๊ฐ€ ์œ ์‚ฌํ•œ ํ…์ŠคํŠธ
  • ๋จผ ๋ฒกํ„ฐ = ์˜๋ฏธ๊ฐ€ ๋‹ค๋ฅธ ํ…์ŠคํŠธ

Sonamu์—์„œ์˜ ํ๋ฆ„

// 1. ๋ฌธ์„œ ์ €์žฅ ์‹œ: ํ…์ŠคํŠธ โ†’ ์ž„๋ฒ ๋”ฉ โ†’ DB
await DocumentModel.saveOne({
  title: "Sonamu ์‹œ์ž‘ํ•˜๊ธฐ",
  content: "...",
  embedding: [0.2, 0.8, -0.3, ...],  // 1024๊ฐœ
});

// 2. ๊ฒ€์ƒ‰ ์‹œ: ์ฟผ๋ฆฌ โ†’ ์ž„๋ฒ ๋”ฉ โ†’ ์œ ์‚ฌ๋„ ๊ณ„์‚ฐ
const queryEmbedding = [0.3, 0.7, -0.2, ...];
const results = await searchSimilar(queryEmbedding);

Embedding ํด๋ž˜์Šค

Sonamu๋Š” ์ž„๋ฒ ๋”ฉ์„ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” Embedding ํด๋ž˜์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:
import { Embedding } from "sonamu/vector";

// ๋‹จ์ผ ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ
const result = await Embedding.embedOne(
  "Sonamu๋Š” TypeScript ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค",
  'voyage'  // 'voyage' | 'openai'
);

console.log(result.embedding);  // [0.123, -0.456, ...] (1024๊ฐœ)
console.log(result.model);      // "voyage-3"
console.log(result.tokenCount); // 8

์–ด๋–ค ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์„ ํƒํ• ๊นŒ?

Voyage AI vs OpenAI

Sonamu๋Š” ๋‘ ๊ฐ€์ง€ ์ž„๋ฒ ๋”ฉ ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค:
ํ•ญ๋ชฉVoyage AIOpenAI
ํ•œ๊ตญ์–ด ์„ฑ๋Šฅโญโญโญโญโญ ์ตœ๊ณ โญโญโญโญ ์ข‹์Œ
์˜์–ด ์„ฑ๋Šฅโญโญโญโญโญโญโญโญโญโญ
์ฐจ์›10241536
์ตœ๋Œ€ ํ† ํฐ32,0008,191
๋ฐฐ์น˜ ํฌ๊ธฐ128100
๋น„๋Œ€์นญ ์ž„๋ฒ ๋”ฉโœ… (document/query ๊ตฌ๋ถ„)โŒ
Sonamu ์ถ”์ฒœโญโญโญโญโญโญโญโญโญ

Sonamu ํ”„๋กœ์ ํŠธ ๊ธฐ์ค€ ์„ ํƒ

Voyage AI ์ถ”์ฒœ:
  • โœ… ํ•œ๊ตญ์–ด ์„œ๋น„์Šค (ํ•œ๊ตญ์–ด ์„ฑ๋Šฅ ์šฐ์ˆ˜)
  • โœ… ๊ธด ๋ฌธ์„œ ์ฒ˜๋ฆฌ (32,000 ํ† ํฐ)
  • โœ… ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ์ค‘์š” (๋น„๋Œ€์นญ ์ž„๋ฒ ๋”ฉ)
OpenAI ์ถ”์ฒœ:
  • โœ… ๊ธ€๋กœ๋ฒŒ ์„œ๋น„์Šค (๋‹ค๊ตญ์–ด ๊ท ํ˜•)
  • โœ… ์ด๋ฏธ OpenAI API ์‚ฌ์šฉ ์ค‘

ํ™˜๊ฒฝ ์„ค์ •

1. ํŒจํ‚ค์ง€ ์„ค์น˜

pnpm add voyageai
# ๋˜๋Š”
pnpm add @ai-sdk/openai

2. API ํ‚ค ์„ค์ •

# Voyage AI (๊ถŒ์žฅ)
VOYAGE_API_KEY=pa-...

# OpenAI (๋Œ€์•ˆ)
OPENAI_API_KEY=sk-...
API ํ‚ค๋Š” ๊ฐ ํ”„๋กœ๋ฐ”์ด๋” ์›น์‚ฌ์ดํŠธ์—์„œ ๋ฐœ๊ธ‰๋ฐ›์Šต๋‹ˆ๋‹ค:

Sonamu Model์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

๋ฌธ์„œ ์ €์žฅ ์‹œ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ

import { BaseModel, api } from "sonamu";
import { Embedding } from "sonamu/vector";

class DocumentModelClass extends BaseModel {
  @api({ httpMethod: 'POST' })
  async createDocument(title: string, content: string) {
    // 1. ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
    const result = await Embedding.embedOne(
      `${title}\n\n${content}`,
      'voyage',
      'document'  // ๋ฌธ์„œ์šฉ ์ž„๋ฒ ๋”ฉ
    );
    
    // 2. DB์— ์ €์žฅ (Sonamu์˜ saveOne)
    const doc = await this.saveOne({
      title,
      content,
      embedding: result.embedding,  // 1024๊ฐœ ์ˆซ์ž ๋ฐฐ์—ด
      token_count: result.tokenCount,
    });
    
    return doc;
  }
}
ํ๋ฆ„:
  1. ์‚ฌ์šฉ์ž๊ฐ€ POST /documents์— ๋ฌธ์„œ ์—…๋กœ๋“œ
  2. Sonamu API์—์„œ Voyage AI๋กœ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
  3. PostgreSQL์— ํ…์ŠคํŠธ + ์ž„๋ฒ ๋”ฉ ํ•จ๊ป˜ ์ €์žฅ

๊ฒ€์ƒ‰ API (๋‚˜์ค‘์— ๊ตฌํ˜„)

@api({ httpMethod: 'POST' })
async search(query: string) {
  // 1. ์ฟผ๋ฆฌ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
  const result = await Embedding.embedOne(
    query,
    'voyage',
    'query'  // ๊ฒ€์ƒ‰์šฉ ์ž„๋ฒ ๋”ฉ
  );
  
  // 2. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ (vector-search.mdx ์ฐธ๊ณ )
  const results = await this.getPuri().raw(`
    SELECT title, content,
      1 - (embedding <=> ?) AS similarity
    FROM documents
    WHERE embedding IS NOT NULL
    ORDER BY similarity DESC
    LIMIT 10
  `, [JSON.stringify(result.embedding)]);
  
  return results.rows;
}

๋น„๋Œ€์นญ ์ž„๋ฒ ๋”ฉ (Voyage AI)

Voyage AI๋Š” ๋ฌธ์„œ์™€ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ž„๋ฒ ๋”ฉํ•ฉ๋‹ˆ๋‹ค.

์™œ ๊ตฌ๋ถ„ํ• ๊นŒ?

๋ฌธ์„œ (document):
  • ๊ธด ํ…์ŠคํŠธ
  • ์ƒ์„ธ ์ •๋ณด
  • ์ €์žฅ ๋ชฉ์ 
์ฟผ๋ฆฌ (query):
  • ์งง์€ ํ…์ŠคํŠธ
  • ๊ฒ€์ƒ‰์–ด
  • ๊ฒ€์ƒ‰ ๋ชฉ์ 
๋ฌธ์„œ์™€ ์ฟผ๋ฆฌ๋Š” ์„ฑ๊ฒฉ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค. Voyage AI๋Š” ์ด๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ๋” ์ •ํ™•ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค (10-15% ํ–ฅ์ƒ).

Sonamu์—์„œ ์‚ฌ์šฉ

// ๋ฌธ์„œ ์ €์žฅ ์‹œ: document
const docEmbedding = await Embedding.embedOne(
  "Sonamu๋Š” TypeScript ํ’€์Šคํƒ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. API, DB, ์ธ์ฆ ๋“ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.",
  'voyage',
  'document'  // ๋ฌธ์„œ์šฉ
);

// ๊ฒ€์ƒ‰ ์‹œ: query
const queryEmbedding = await Embedding.embedOne(
  "TypeScript ํ”„๋ ˆ์ž„์›Œํฌ",
  'voyage',
  'query'  // ๊ฒ€์ƒ‰์šฉ
);
OpenAI๋Š” ๋น„๋Œ€์นญ ์ž„๋ฒ ๋”ฉ์„ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค:
// OpenAI๋Š” inputType์ด ๋ฌด์‹œ๋จ
await Embedding.embedOne(text, 'openai', 'document');  // 'document' ๋ฌด์‹œ

๋ฐฐ์น˜ ์ฒ˜๋ฆฌ

์—ฌ๋Ÿฌ ๋ฌธ์„œ๋ฅผ ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ๋•Œ:
@api({ httpMethod: 'POST' })
async batchCreateDocuments(documents: Array<{
  title: string;
  content: string;
}>) {
  // 1. ํ…์ŠคํŠธ ๋ฐฐ์—ด ์ค€๋น„
  const texts = documents.map(doc => 
    `${doc.title}\n\n${doc.content}`
  );
  
  // 2. ๋ฐฐ์น˜ ์ž„๋ฒ ๋”ฉ (์ž๋™์œผ๋กœ 128๊ฐœ์”ฉ ๋ถ„ํ• )
  const embeddings = await Embedding.embed(
    texts,
    'voyage',
    'document'
  );
  
  // 3. DB์— ์ €์žฅ
  const savedDocs = await Promise.all(
    documents.map((doc, i) => 
      this.saveOne({
        title: doc.title,
        content: doc.content,
        embedding: embeddings[i].embedding,
        token_count: embeddings[i].tokenCount,
      })
    )
  );
  
  return savedDocs;
}
์ž๋™ ๋ถ„ํ• :
  • Voyage AI: 128๊ฐœ์”ฉ
  • OpenAI: 100๊ฐœ์”ฉ
1000๊ฐœ ๋ฌธ์„œ๋„ ์ž๋™์œผ๋กœ ๋‚˜๋ˆ ์„œ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ง„ํ–‰๋ฅ  ํ‘œ์‹œ

๋งŽ์€ ๋ฌธ์„œ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์ง„ํ–‰๋ฅ ์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
@api({ httpMethod: 'POST' })
async importDocuments(files: string[]) {
  const texts = files.map(f => readFile(f));
  
  const embeddings = await Embedding.embed(
    texts,
    'voyage',
    'document',
    (processed, total) => {
      const percent = Math.round((processed / total) * 100);
      console.log(`Progress: ${processed}/${total} (${percent}%)`);
    }
  );
  
  // Progress: 128/1000 (13%)
  // Progress: 256/1000 (26%)
  // ...
  // Progress: 1000/1000 (100%)
}

์‹ค์ „ ์‹œ๋‚˜๋ฆฌ์˜ค

์‹œ๋‚˜๋ฆฌ์˜ค: ๊ณ ๊ฐ ์ง€์› ์ง€์‹ ๋ฒ ์ด์Šค

Sonamu๋กœ ๊ณ ๊ฐ ์ง€์› ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. 1๋‹จ๊ณ„: ๋ฌธ์„œ ์—…๋กœ๋“œ API
class KnowledgeBaseModelClass extends BaseModel {
  @upload({ mode: 'single' })
  @api({ httpMethod: 'POST' })
  async uploadDocument() {
    const { file } = Sonamu.getUploadContext();
    
    // ํŒŒ์ผ ์ฝ๊ธฐ
    const content = await file.toBuffer().then(b => b.toString());
    
    // ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
    const embedding = await Embedding.embedOne(
      content,
      'voyage',
      'document'
    );
    
    // ์ €์žฅ
    return await this.saveOne({
      title: file.filename,
      content,
      embedding: embedding.embedding,
    });
  }
}
2๋‹จ๊ณ„: ๊ฒ€์ƒ‰ API
@api({ httpMethod: 'POST' })
async searchSimilar(query: string) {
  // ์ฟผ๋ฆฌ ์ž„๋ฒ ๋”ฉ
  const embedding = await Embedding.embedOne(query, 'voyage', 'query');
  
  // ์œ ์‚ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰
  const results = await this.getPuri().raw(`
    SELECT id, title, content,
      1 - (embedding <=> ?) AS similarity
    FROM knowledge_base
    WHERE embedding IS NOT NULL
    ORDER BY similarity DESC
    LIMIT 5
  `, [JSON.stringify(embedding.embedding)]);
  
  return results.rows;
}
3๋‹จ๊ณ„: ์‚ฌ์šฉ์ž ์š”์ฒญ ์ฒ˜๋ฆฌ
POST /api/knowledge-base/search
{ "query": "ํ™˜๋ถˆ ์ •์ฑ…์ด ๊ถ๊ธˆํ•ด์š”" }

โ†’ ์œ ์‚ฌ ๋ฌธ์„œ 5๊ฐœ ๋ฐ˜ํ™˜:
1. "ํ™˜๋ถˆ ๋ฐ ๊ตํ™˜ ์•ˆ๋‚ด" (similarity: 0.89)
2. "๊ตฌ๋งค ์ทจ์†Œ ๋ฐฉ๋ฒ•" (similarity: 0.82)
3. "๊ฒฐ์ œ ์ˆ˜๋‹จ๋ณ„ ํ™˜๋ถˆ ๊ธฐ๊ฐ„" (similarity: 0.78)
...

์—๋Ÿฌ ์ฒ˜๋ฆฌ

@api({ httpMethod: 'POST' })
async createDocument(title: string, content: string) {
  try {
    const embedding = await Embedding.embedOne(
      `${title}\n\n${content}`,
      'voyage',
      'document'
    );
    
    return await this.saveOne({
      title,
      content,
      embedding: embedding.embedding,
    });
  } catch (error) {
    if (error.message.includes('API_KEY')) {
      throw new Error('Voyage API ํ‚ค๋ฅผ ์„ค์ •ํ•˜์„ธ์š”: VOYAGE_API_KEY');
    } else if (error.message.includes('token')) {
      throw new Error('ํ…์ŠคํŠธ๊ฐ€ ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค (์ตœ๋Œ€ 32,000 ํ† ํฐ)');
    } else {
      throw new Error(`์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์‹คํŒจ: ${error.message}`);
    }
  }
}

๋น„์šฉ ๊ณ ๋ ค

ํ† ํฐ ๊ณ„์‚ฐ

const result = await Embedding.embedOne(
  "Sonamu๋Š” TypeScript ํ’€์Šคํƒ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค",
  'voyage'
);

console.log(`Tokens: ${result.tokenCount}`);  // 12

๋น„์šฉ ์˜ˆ์ธก

Voyage AI ($0.13 per 1M tokens):
// 1000๊ฐœ ๋ฌธ์„œ, ํ‰๊ท  500ํ† ํฐ
// = 500,000 ํ† ํฐ
// = $0.065
OpenAI ($0.02 per 1M tokens):
// 1000๊ฐœ ๋ฌธ์„œ, ํ‰๊ท  500ํ† ํฐ
// = 500,000 ํ† ํฐ
// = $0.01

๋น„์šฉ ์ ˆ๊ฐ ํŒ

1. ์บ์‹ฑ
const cache = new Map<string, number[]>();

async function getCachedEmbedding(text: string) {
  if (cache.has(text)) {
    return cache.get(text)!;
  }
  
  const result = await Embedding.embedOne(text, 'voyage');
  cache.set(text, result.embedding);
  return result.embedding;
}
2. ์ค‘๋ณต ์ œ๊ฑฐ
// ๊ฐ™์€ ํ…์ŠคํŠธ๋Š” ํ•œ ๋ฒˆ๋งŒ ์ž„๋ฒ ๋”ฉ
const uniqueTexts = [...new Set(texts)];
const embeddings = await Embedding.embed(uniqueTexts, 'voyage');

์ฃผ์˜์‚ฌํ•ญ

Sonamu์—์„œ ์ž„๋ฒ ๋”ฉ ์‚ฌ์šฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. API ํ‚ค ํ•„์ˆ˜: ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •
    VOYAGE_API_KEY=pa-...
    
  2. ์ฐจ์› ์ˆ˜ ์ผ์น˜: DB ์Šคํ‚ค๋งˆ์™€ ๋งž์ถฐ์•ผ ํ•จ
    -- Voyage AI
    CREATE TABLE docs (embedding vector(1024));
    
    -- OpenAI
    CREATE TABLE docs (embedding vector(1536));
    
  3. document vs query: Voyage๋Š” ๊ตฌ๋ถ„, OpenAI๋Š” ๋ฌด์‹œ
    // ์ €์žฅ ์‹œ
    await Embedding.embedOne(text, 'voyage', 'document');
    
    // ๊ฒ€์ƒ‰ ์‹œ
    await Embedding.embedOne(text, 'voyage', 'query');
    
  4. ํ† ํฐ ์ œํ•œ: ๋„ˆ๋ฌด ๊ธด ํ…์ŠคํŠธ๋Š” ์ฒญํ‚น ํ•„์š”
    • Voyage AI: 32,000 ํ† ํฐ
    • OpenAI: 8,191 ํ† ํฐ
  5. NULL ์ฒ˜๋ฆฌ: ์ž„๋ฒ ๋”ฉ ์‹คํŒจ ์‹œ NULL ์ €์žฅ ๊ฐ€๋Šฅ
    embedding: result?.embedding || null
    
  6. ๋น„์šฉ ๋ชจ๋‹ˆํ„ฐ๋ง: ํ† ํฐ ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์ 
    console.log(`Total tokens: ${result.tokenCount}`);
    

๋‹ค์Œ ๋‹จ๊ณ„

์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ Sonamu Model์—์„œ ๊ฒ€์ƒ‰ API๋ฅผ ๊ตฌํ˜„ํ•  ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค.