본문으로 건너뛰기
페이지 목차
이 페이지에서 검색

멀티 에이전트

이 페이지에 묻기

멀티 에이전트 · 현재 차시와 목차 본문에서만 찾아 답합니다. 답을 지어내지 않고 해당하는 대목을 그대로 보여 주며, 여기에 없으면 없다고 알려 드립니다.

생성형 AI가 아니라 이 페이지 본문을 찾아 그대로 보여 주는 방식입니다. 외부로 나가는 요청이 없습니다.

MULTI · AGENT

멀티 에이전트

생성 수정

도구 실행·검색·기억·그래프 흐름·전문 에이전트의 협업을 구성하고 권한·평가·사람의 확인으로 통제합니다.

26개 문서 · 1일차 · 7 / 26

6차시 · 데이터 조회 도구 구현

지도 위치 : 1일차 · README 4장 ② 행동 층 — 실제 사내 데이터에 닿는 도구 | 이전: 5차시 Function Calling 원리 | 다음: 7차시 다중 도구 라우팅 키워드 : 도구 함수 설계, 인자 스키마, 결과 반환

이 차시를 다 읽으면

  1. 좋은 도구의 네 원칙( 한 가지 일 · 이름과 설명이 곧 프롬프트 · 인자는 최소 · enum )으로 도구 설계를 검토할 수 있다.
  2. 읽기 ↔ 쓰기 축으로 도구를 나누고, 왜 조회 도구부터 만드는지 말할 수 있다.
  3. 모델에게 SQL을 쓰게 하지 않고 정해진 함수 로 사내 DB에 연결하는 이유를 설명할 수 있다.
  4. 모델이 채운 인자를 Pydantic으로 다시 검증 하는 이유를 말할 수 있다.
  5. 도구 결과를 필요한 필드만 · 짧게 · 개인정보 최소로 돌려주고, "없음"과 "오류"(is_error)를 구별할 수 있다.

0. 이 차시의 판별 축

#축왼쪽 (0)오른쪽 (1)이 축으로 갈리는 것큰 축 (README 5장)
1⭐ 읽기 ↔ 쓰기조회 도구 (상태를 안 바꿈, 재시도 안전)변경 도구 (상태를 바꿈, 재시도 위험)자동 실행해도 되는가5.4 파생 축 (부모 ⑦)
2값인가 동작인가도구 결과 (돌려주는 데이터)도구 실행 (DB 조회 동작)모델에게 무엇을 보여 주는가③ 속성 ↔ 기능
3부탁 ↔ 강제모델이 SQL을 직접 씀정해진 함수만 부를 수 있음모델이 할 수 있는 일의 범위5.4 파생 축 (부모 ②)
4무엇이 밖으로 나가는가내 DB의 전체 레코드외부 API로 보내는 도구 결과결과에 무엇을 담을 것인가④ 로컬 ↔ 원격

이 차시의 질문은 "모델이 실제 사내 데이터를 쓰게 하되, 안전하고 쓸모 있게 하려면 도구를 어떻게 만들어야 하나?" 입니다. 앞에서 도구 호출 한 바퀴를 돌렸습니다(5차시). 이번에는 그 도구의 안쪽 (함수 설계 · 인자 검증 · 결과 형식)을 만듭니다.


1. 좋은 도구 설계

1.1 정의

도구 함수 — [앱의 함수]인데, [ 모델의 요청을 받아 실행되도록 이름 · 설명 · 인자 스키마와 함께 공개된다 ]는 점에서 앱 내부에서만 쓰이는 일반 함수와 구별된다.

1.2 네 원칙

원칙뜻어기면
한 가지 일도구 하나는 한 종류의 일만hr_tool(action=조회·신청·취소) 같은 만능 도구 → 읽기와 쓰기가 섞여 권한을 따로 걸 수 없음
이름 · 설명이 곧 프롬프트모델은 이름과 설명만 보고 고름. 언제 쓰고 언제 쓰지 말지 까지비슷한 도구 사이에서 엉뚱한 선택 (7차시)
인자는 최소 · 명확모델이 채워야 하는 인자는 적을수록 좋음. 코드가 알 수 있는 값(사번, 오늘 날짜)은 인자로 받지 않음모델이 사번을 지어내거나, 남의 사번을 넣음
enum값이 정해져 있으면 목록으로 고정"진행중", "처리 중", "in progress" 제각각

1.3 ❌ / ⭕

# ❌ 만능 · 모호
{"name": "query", "description": "데이터를 조회한다",
 "input_schema": {"type": "object", "properties": {"sql": {"type": "string"}}}}

# ⭕ 한 가지 일 · 언제 쓸지까지
{"name": "get_ticket_status",
 "description": "현재 사용자가 접수한 IT 지원 티켓의 상태를 조회한다. "
                "'내 티켓', '접수한 장애 어떻게 됐어' 같은 질문에 사용. "
                "새 장애 접수에는 사용하지 않는다.",
 "input_schema": {
     "type": "object",
     "properties": {
         "status_filter": {"type": "string", "enum": ["전체", "처리중", "완료"],
                           "description": "어떤 상태의 티켓을 볼지. 언급이 없으면 전체"}},
     "required": ["status_filter"],
     "additionalProperties": False},
 "strict": True}

🔍 읽는 포인트 : ⭕ 쪽의 description 마지막 문장 "새 장애 접수에는 사용하지 않는다" — 쓰지 말 때를 적었습니다. 그리고 enum 세 값. 사번 인자는 없습니다.


2. ⭐ 읽기 ↔ 쓰기 — 조회 도구부터

읽기 (조회)쓰기 (변경)
오피스봇 예get_leave_balance, get_ticket_statusrequest_equipment, request_leave
상태를 바꾸나아니오예 — 기록이 남음
같은 요청을 두 번 보내면같은 결과, 피해 없음두 번 신청됨
모델이 잘못 고르면엉뚱한 정보를 보여 줌 (답에서 드러남)엉뚱한 신청이 실제로 들어감
실행 방식자동사람 확인 후 (1차시 자동화 범위)

⭐ 첫 버전은 조회 도구만 붙입니다. 모델이 도구를 잘못 고르거나, 10차시에서 볼 프롬프트 인젝션에 속더라도 조회밖에 못 하면 사고가 작습니다.

앞으로 다시 만납니다 — 쓰기 도구는 8차시에서 읽기 도구와 분리된 목록 으로 두고, 23차시에서 승인 게이트를 거쳐 실행합니다.


3. 사내 DB 연동 — 모델에게 SQL을 쓰게 하지 않는다

3.1 왜

모델이 SQL을 쓰게 하면정해진 함수로 감싸면
SELECT * FROM salary — 볼 수 없는 테이블도 조회 가능함수가 정한 테이블 · 컬럼만
WHERE emp_id = '다른 사람' — 남의 데이터사번은 코드가 세션에서 넣음
DELETE, UPDATE가 섞일 수 있음조회 함수 안에는 조회 문장만
같은 질문에 매번 다른 SQL → 시험이 어려움같은 인자 → 같은 쿼리 (결정론, 3차시)

이것이 세 문장 중 문장 2 — 모델은 판단하고(어떤 도구 · 어떤 연도), 코드는 지킨다(무엇을 어떻게 조회) 의 모양입니다.

3.2 함수

def get_leave_balance(session_user: str, year: int) -> dict:
    row = db.execute(
        "SELECT granted, used FROM leave_balance WHERE emp_id = ? AND year = ?",
        (session_user, year),                         # 자리표시자 — 값은 따로 전달
    ).fetchone()
    if row is None:
        return {"found": False, "message": f"{year}년 연차 기록이 없습니다."}
    return {"found": True, "year": year, "remaining_days": row.granted - row.used}

🔍 읽는 포인트

  • 첫 인자 session_user — 모델이 아니라 로그인 세션 에서 옵니다(8차시).
  • ? 자리표시자 — 값을 SQL 문장에 이어 붙이지 않습니다. 이어 붙이면 값 속에 SQL을 숨겨 넣는 공격(SQL 인젝션)이 가능해집니다.
  • remaining_days를 코드가 계산 합니다. 뺄셈을 모델에게 맡기지 않습니다(결정론, 3차시).

4. 인자 검증 — 모델이 채운 값도 바깥 입력이다

strict: True는 모양 (정수인가, enum 안인가)을 보장합니다(5차시). 그러나 값의 범위 는 모릅니다. year: 1999도 정수입니다.

from pydantic import BaseModel, Field, ValidationError

class LeaveBalanceArgs(BaseModel):
    year: int = Field(ge=2020, le=2030, description="조회 연도")

def run_get_leave_balance(raw_input: dict, session_user: str) -> dict:
    try:
        args = LeaveBalanceArgs.model_validate(raw_input)
    except ValidationError:
        return {"is_error": True, "content": "연도는 2020~2030 사이여야 합니다."}
    return {"content": get_leave_balance(session_user, args.year)}

🔍 읽는 포인트 : model_validate(raw_input) 한 줄이 문지기 입니다. 실패하면 함수를 실행하지 않고 고칠 수 있는 메시지 를 돌려줍니다. 모델은 이 메시지를 읽고 인자를 고쳐 다시 요청할 수 있습니다.

검사누가예
형식 (타입 · enum)strict: Trueyear가 정수인가
범위 · 업무 규칙Pydantic 검증 (코드)2020~2030인가
권한도구 코드 (8차시)이 사용자가 볼 수 있는 데이터인가

5. 결과 반환

5.1 원칙

원칙왜
필요한 필드만결과는 다음 호출의 입력 토큰 이 됨(2차시). 필요 없는 필드는 비용이고, 모델을 헷갈리게 함
짧게, 뜻이 분명한 이름rem 보다 remaining_days. 모델이 읽고 문장으로 바꿀 것이므로
개인정보 최소화결과는 외부 LLM API로 나갑니다 (축 ④). 주민번호 · 연락처 · 연봉은 질문에 필요 없으면 넣지 않음
없을 때도 문장으로빈 결과 []만 주면 모델이 "아마 없을 것"이라며 추측하거나 지어냄

5.2 ❌ / ⭕

# ❌ 레코드 통째로
{"emp_id": "E1024", "name": "김민수", "rrn": "900101-1******", "phone": "010-....",
 "dept": "재무팀", "salary_grade": "S3", "granted": 15, "used": 8, "updated_at": "..."}

# ⭕ 질문에 필요한 것만
{"found": True, "year": 2026, "remaining_days": 7}

🔍 읽는 포인트 : ❌ 쪽은 연차를 물었는데 주민번호 · 전화 · 급여 등급까지 외부로 보냅니다. 모델이 답에 쓰지 않더라도 이미 나간 것입니다.

5.3 "없음"과 "오류"를 구별

상황결과is_error모델이 할 일
조회 성공{"found": True, "remaining_days": 7}없음답한다
정상적으로 없음 (티켓이 0건){"found": False, "message": "접수한 티켓이 없습니다."}없음"없습니다"라고 답한다
조회 자체가 실패 (DB 타임아웃)"오류: 인사 시스템에 연결하지 못했습니다. 잠시 후 다시 시도해 주세요."True실패를 알리고 숫자를 만들지 않는다
{"type": "tool_result", "tool_use_id": block.id,
 "content": "오류: 인사 시스템에 연결하지 못했습니다.", "is_error": True}

🔍 읽는 포인트 : is_error: True 한 줄. ⚠️ "없음"을 오류로 보내면 모델은 "시스템 오류"라고 안내하고, "오류"를 "없음"으로 보내면 "티켓이 없습니다"라고 틀린 사실을 말합니다.

5.4 오피스봇의 두 조회 도구

get_leave_balanceget_ticket_status
모델이 채우는 인자year (정수, 2020~2030)status_filter (enum: 전체 · 처리중 · 완료)
코드가 채우는 값사번 (세션)사번 (세션)
돌려주는 필드found, year, remaining_daysfound, 티켓별 ticket_id, title, status, updated (최근 5건)
없을 때"{year}년 연차 기록이 없습니다.""접수한 티켓이 없습니다."
오류 때is_error + "인사 시스템 연결 실패"is_error + "헬프데스크 시스템 연결 실패"

💡 get_ticket_status는 티켓이 수십 건일 수 있으므로 최근 5건 처럼 상한을 둡니다. 결과 길이가 곧 비용입니다.

앞으로 다시 만납니다 — 두 도구가 나란히 놓이면 모델이 어느 쪽을 고르는가 가 문제가 됩니다. 7차시에서 도구 선택 정확도를 잽니다.


6. 실습 — 맡기고 검수하기

지시

오피스봇에 조회 도구 두 개(get_leave_balance, get_ticket_status)를 실제 DB 연동 구조로 만들어줘.

참고 공식 문서: Anthropic Tool use 문서(input_schema, strict, is_error), Pydantic 공식 문서, Python sqlite3 문서

목표:
1. SQLite 가짜 DB: 직원 5명, leave_balance(emp_id, year, granted, used), tickets(ticket_id, emp_id, title, status, updated)
2. 도구 함수는 사번을 인자로 받지 말고 session_user로 받을 것. SQL은 자리표시자(?)만 사용
3. 인자는 Pydantic으로 다시 검증 (year 2020~2030, status_filter enum)
4. 결과는 표에 정한 필드만. 없음은 found False + 문장, 시스템 오류는 is_error True
5. get_ticket_status는 최근 5건까지만
테스트: ① 연차 조회 ② 기록 없는 연도 ③ year=1999 ④ 티켓 처리중만 ⑤ 티켓 0건인 직원 ⑥ DB 파일을 잠시 없애 오류 유도

제약: 모델이 SQL을 쓰는 도구를 만들지 말 것. 쓰기 도구는 이번에 만들지 말 것.
추측하지 말고 실행 명령과 실행 출력 원문을 붙여.
보고 형식: [시나리오 | 모델이 채운 인자 | 검증 결과 | tool_result 원문 | is_error | 최종 답] 표

받을 증거

  • 시나리오 6개의 표와 tool_result 원문 (모델에게 실제로 보낸 글)
  • 두 도구의 선언 JSON 원문

내가 직접 확인할 것

볼 것이걸로 아는 것
도구 선언에 사번 · 이름 인자가 없는가1.2 — 신원을 모델에게 맡기지 않는가
tool_result 원문에 주민번호 · 연락처 · 급여가 한 글자도 없는가5.2 — 무엇이 외부로 나가는가
⑤의 최종 답이 "없습니다", ⑥의 최종 답이 "시스템 오류"로 서로 다른가5.3 — 없음과 오류를 구별하는가
⑥에서 모델이 연차 숫자를 지어내지 않았는가5.3 — is_error가 환각을 막는가
③에서 함수가 실행되지 않고 고칠 수 있는 메시지가 갔는가4 — 인자 검증 문지기
코드에서 SQL 문자열에 +나 f-string으로 값을 붙인 곳이 0건 인가3.2 — 자리표시자만 쓰는가

부록 A. 스스로 확인하기

#질문답이 있는 곳
1좋은 도구의 네 원칙은?1.2
2도구 설명에 "언제 쓰지 말지"까지 적는 이유는?1.2, 1.3
3읽기 도구부터 만드는 이유는?2
4모델에게 SQL을 쓰게 하면 생기는 문제 네 가지는?3.1
5? 자리표시자를 쓰는 이유는?3.2
6strict가 있는데도 Pydantic 검증이 필요한 이유는?4
7결과 반환의 네 원칙은?5.1
8레코드를 통째로 돌려주면 무엇이 문제인가?5.2
9"없음"과 "오류"를 어떻게 다르게 돌려주나?5.3

부록 B. 자주 하는 실수

실수바로잡기
조회 · 신청 · 취소를 한 도구에❌ 한 가지 일. 읽기와 쓰기를 분리 (1.2, 2)
사번을 도구 인자로❌ 코드가 세션에서 (1.2, 3.2)
"SQL을 쓰게 하면 유연하다"❌ 권한 · 삭제 · 재현성 문제. 정해진 함수로 (3.1)
SQL에 값을 문자열로 이어 붙임❌ 자리표시자 (3.2)
strict만 믿고 값 검증 생략⚠️ 모양 ≠ 범위. Pydantic으로 (4)
DB 레코드를 통째로 결과에❌ 필요한 필드만. 외부로 나감 (5.2)
조회 실패를 빈 결과로❌ is_error: True (5.3)

부록 C. NotebookLM 복습 프롬프트

이 6차시 노트만 근거로 6문제를 내줘. 비전공자 기준으로.
"좋은 도구의 네 원칙", "읽기 도구부터", "SQL을 모델에게 맡기지 않는 이유", "strict와 Pydantic 검증의 차이", "없음과 오류의 구별"은 반드시 포함해줘.
답은 내가 답한 다음에 채점하고, 틀린 이유를 노트의 절 번호를 근거로 설명해줘.