본문으로 건너뛰기
김신건의 로그

[Voice AI] 음성 우선 프롬프트 + Function Calling + Structured Output

· 수정 · 📖 약 1분 · 328자/단어 #prompt #function-calling #structured-output #voice-llm
voice-first prompt, function calling, tool use, structured output, JSON schema, voice prompt design, persona

음성 우선 프롬프트 설계

음성 답변 = 귀로 듣는다. 텍스트 답변과 완전 다르게 짜야 한다.

flowchart LR
    Text["텍스트 답변 (긴 답변 OK)"] --> A1["**bold**, # 헤딩, 표"]
    Voice["음성 답변 (짧고 자연스럽게)"] --> A2["1-2 문장, 의문문으로 turn 넘김"]
    A1 --> Diff
    A2 --> Diff
    Diff{둘은 완전 다름}

음성 프롬프트의 핵심 원칙

원칙의미
짧게한 답변 1-2 문장. 50-80 글자
자연스럽게”$5 = ‘오 달러’” (숫자 정규화 사전 정의)
마크다운 금지**, #, - 등 모두 음성에 안 들어감
listicle 분해”1, 2, 3…” 대신 대화로 분배
turn 넘기기질문으로 끝내거나 “도와드릴까요?”
에스컬레이션”사람 상담원 연결해드릴까요?” 명시
SYSTEM_PROMPT = """
당신은 친절한 한국어 콜센터 상담원입니다.

응답 규칙:
- 한 응답은 *1-2 문장* 으로 짧게
- 자연스러운 구어체 사용 ("입니다" 보다 "이에요")
- 숫자는 풀어쓰기 (예: "100만 원" 보다 "백만 원")
- 마크다운, 영어 약어, 코드 사용 금지
- 모르는 정보면 "확인이 어렵네요. 사람 상담원 연결해드릴까요?"
- 답변 끝에 *질문* 으로 turn 을 사용자에게 넘김

페르소나:
- 침착하고 공감하는 톤
- 고객을 "고객님" 으로 호칭
- 절대 "AI" 라고 본인을 칭하지 말 것
"""

Function / Tool Calling

sequenceDiagram
    autonumber
    User->>Agent: "내 주문 상태 확인해줘"
    Agent->>LLM: prompt + tool definitions
    LLM-->>Agent: function_call: get_order_status(user_id=42)
    Agent->>API: GET /orders?user=42
    API-->>Agent: { status: "shipped", eta: "내일" }
    Agent->>LLM: tool_result
    LLM-->>Agent: "고객님 주문은 배송 중이며, 내일 도착 예정이에요."
    Agent->>TTS: 음성 합성
    TTS->>User: 음성 응답

OpenAI Function Calling

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "사용자의 주문 상태를 조회합니다",
            "parameters": {
                "type": "object",
                "properties": {
                    "user_id": {"type": "integer", "description": "사용자 ID"},
                    "order_id": {"type": "string", "description": "주문 ID (선택)"}
                },
                "required": ["user_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "transfer_to_human",
            "description": "사람 상담원에게 연결합니다",
            "parameters": {
                "type": "object",
                "properties": {
                    "reason": {"type": "string"},
                    "priority": {"type": "string", "enum": ["low", "high"]}
                },
                "required": ["reason"]
            }
        }
    }
]

response = await client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    tools=tools,
    tool_choice="auto",
    stream=True,
)

병렬 Function Calling

# 같은 응답에서 여러 함수 동시 호출 가능
{
  "tool_calls": [
    { "id": "call_1", "function": { "name": "get_weather", "arguments": "..." } },
    { "id": "call_2", "function": { "name": "get_traffic", "arguments": "..." } }
  ]
}

# 병렬 실행
results = await asyncio.gather(
    get_weather(...),
    get_traffic(...),
)

직렬 보다 수배 빠름. 독립 호출이면 활용.

결과를 대화에 자연스럽게

@function_tool
async def get_order_status(user_id: int) -> dict:
    order = await db.get_order(user_id)
    return {
        "status": order.status,
        "eta": order.eta,
        # 함수 결과는 *데이터* 만. *음성 문장* 은 LLM 이 합성
    }

# 잘못된 예: 함수가 문장 반환
@function_tool
async def get_order_status_bad(user_id: int) -> str:
    order = await db.get_order(user_id)
    return f"주문 {order.id}{order.status} 상태이며 ETA {order.eta} 입니다."
    # ← TTS 가 "ETA" 를 영어로 발음하거나 어색

Structured Output

LLM 응답을 JSON schema 강제:

class IntentExtraction(BaseModel):
    intent: Literal["order_status", "refund", "complaint", "transfer", "other"]
    user_id: Optional[int]
    order_id: Optional[str]
    urgency: Literal["low", "normal", "high"]

response = await client.chat.completions.parse(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": user_text}],
    response_format=IntentExtraction,
)

intent = response.choices[0].message.parsed
# intent.intent == "order_status"  ← 타입 안전

슬롯 채우기 (Slot Filling)

# 사용자가 부분 정보만 주면 → 부족한 슬롯 묻기
class BookingRequest(BaseModel):
    restaurant: Optional[str]
    date: Optional[str]
    party_size: Optional[int]
    is_complete: bool = False

# 첫 발화: "내일 5명 예약하고싶어"
# extracted: { date: "내일", party_size: 5, restaurant: None, is_complete: false }

# LLM 이 부족한 정보 *질문*
if not extracted.restaurant:
    response = "어느 식당으로 예약 도와드릴까요?"

페르소나 일관성

PERSONA = """
이름: 민지
성격: 친절, 차분, 약간 유머
역할: 카페 음성 주문 도우미
말투: 격식 있는 존댓말 ("이에요", "괜찮으세요?")
금지: 비속어, 정치/종교, 의료 조언
"""

SYSTEM = f"""
{PERSONA}

응답 규칙:
- 항상 페르소나 일관성 유지
- 메뉴 외 질문은 정중히 거절 + 본업으로 유도
"""

흔한 함정

WARNING

  1. 시스템 프롬프트가 길고 마크다운 = TTS 가 안 읽지만, LLM 의 응답이 마크다운 따라함. 출력 형식 명시.
  2. 숫자 발음 = “1500000원” → “일오영영영영영원”. 별도 정규화 또는 SSML.
  3. function 결과로 긴 텍스트 반환 = LLM 이 그대로 읽음. 데이터만.
  4. tool calling 중 interruption 허용 = DB write 중간에 끊김. disallow_interruptions.

관련 위키

이 글의 용어 (4개)
[Voice AI] 음성 에이전트 아키텍처: Cascading / Streaming / S2S + Latency Budgetai
정의 실시간 음성 AI 의 3가지 패턴. 각자 지연 vs 유연성 vs 비용 트레이드오프. 3 패턴 비교 | | Cascading | Streaming | S2S | |---|--…
[Voice AI] Agent 패턴: Supervisor, Handoff, ReAct, HITLai
정의 복잡한 음성 에이전트 = 여러 sub-agent + 조정 패턴. 단일 LLM 으로 처리하기 어려운 흐름. [!IMPORTANT] 각 sub-agent 내부는 VAD → ST…
[Voice AI] Turn Detection + Barge-in + AEC: 자연스러운 대화voice
정의 자연스러운 음성 대화의 3 요소: 1. Turn Detection - 사용자가 말끝났는지 정확히 판단 2. Barge-in - 에이전트가 말하는 중에 사용자가 끼어들 수 있…
[Voice AI] Voice RAG: 지연 병목 + 예측적 프리페치ai
정의 Voice RAG = 음성 에이전트에 RAG (Retrieval-Augmented Generation) 결합. 벡터 DB 조회 가 지연 누적 의 큰 원인이 되므로 예측적 프…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기