KO ▾

OpenAI 호환 무검열 LLM APIuncensoredapis.com

홈에이전트

무검열 LLM API의 에이전트: 함수 호출, 프레임워크 및 가드레일

에이전트는 루프입니다: 모델이 함수를 요청하고, 당신의 코드가 이를 실행하며, 그 결과가 대화에 다시 반영됩니다. 무검열 LLM API는 표준 채팅 형식으로 함수 호출을 지원하므로, 이 루프를 40줄로 구축한 후 동일한 엔드포인트를 LangChain 또는 LlamaIndex에 연결할 수 있습니다. 이 가이드는 두 가지 방법과 도구 실행을 안전하게 유지하기 위한 확인 방법을 모두 다룹니다.

에 업데이트됨

주요 포인트

  1. 요청에 도구를 전달하고, finish_reason이 tool_calls와 일치하는지 확인한 후 함수를 실행하고 도구 메시지를 추가하며, 모델이 텍스트로 응답할 때까지 반복합니다.
  2. LangChain에는 base_url이 있는 ChatOpenAI가 필요하며, LlamaIndex에는 api_base와 함수 호출 플래그가 설정된 OpenAILike가 필요합니다.
  3. 모델이 호출을 제안하면, 당신의 코드가 실행 여부를 결정합니다. 허용 목록, 유효성 검사, 시간 제한 및 루프 상한을 설정하세요.
  4. 에이전트는 요청 횟수를 증가시키므로, 분당 300회 요청 제한과 100,000토큰 컨텍스트 창에 대비해야 합니다.

함수 호출 루프의 구조

프레임워크를 제거하면 모든 에이전트는 동일한 네 가지 작업을 수행합니다. 대화 내용과 tools 목록을 전송합니다. 모델은 텍스트로 응답하여 작업을 완료했음을 알리거나, tool_calls를 반환하여 실행을 요청합니다. 각 호출을 실행하고 호출의 id를 포함하는 tool 메시지를 각 호출당 하나씩 추가합니다. 그런 다음 더 긴 대화 내용으로 모델을 다시 호출합니다.

분기 신호는 finish_reason입니다. 이것이 tool_calls와 일치하면 어시스턴트 메시지의 content이 null로 설정되고 tool_calls 배열이 포함되며, 각 항목에는 id, 함수 이름 및 JSON arguments 문자열이 포함됩니다. 참조 페이지에서 정확한 구조를 확인할 수 있습니다. 두 가지 세부 사항이 흔히 실수를 유발합니다: arguments는 파싱해야 하는 문자열이며, 호출을 포함하는 어시스턴트 메시지는 도구 결과 앞에 순서대로 유지되어야 합니다.

처음부터 시작하는 완전한 루프

아래 스크립트는 하나의 함수를 정의하고 JSON 스키마로 설명한 후, 모델이 일반 텍스트를 생성할 때까지 반복합니다. API를 가리키는 공식 Python SDK를 사용합니다. 각 반복마다 어시스턴트 메시지를 추가하고, 각 호출당 도구 메시지를 추가하며, 단계 카운터가 무한 루프를 방지합니다.

import json, os
from openai import OpenAI

client = OpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])

TICKETS = {"T-101": "open", "T-102": "closed"}

def ticket_status(ticket_id: str) -> str:
    return TICKETS.get(ticket_id, "unknown ticket")

TOOLS = [{
    "type": "function",
    "function": {
        "name": "ticket_status",
        "description": "Look up the status of a support ticket by id.",
        "parameters": {
            "type": "object",
            "properties": {"ticket_id": {"type": "string"}},
            "required": ["ticket_id"],
        },
    },
}]
REGISTRY = {"ticket_status": ticket_status}

messages = [{"role": "user", "content": "Are tickets T-101 and T-102 still open?"}]

for step in range(6):                      # hard cap on loop turns
    r = client.chat.completions.create(
        model="uncensored", messages=messages, tools=TOOLS, max_tokens=400)
    msg = r.choices[0].message
    messages.append(msg.model_dump(exclude_none=True))
    if r.choices[0].finish_reason != "tool_calls":
        print(msg.content)
        break
    for call in msg.tool_calls:
        fn = REGISTRY.get(call.function.name)
        try:
            args = json.loads(call.function.arguments)
            result = fn(**args) if fn else f"error: unknown tool {call.function.name}"
        except Exception as exc:
            result = f"error: {exc}"
        messages.append({"role": "tool", "tool_call_id": call.id, "content": str(result)})
else:
    print("stopped: step limit reached")

실패 시 루프가 수행하는 작업을 주목하세요: 오류가 문자열로 된 도구 결과로 변환됩니다. 이를 통해 모델이 문제를 파악하고 다시 시도할 수 있습니다. 예를 들어 잘못된 티켓 ID를 수정하는 것과 같습니다. 이는 프로그램이 충돌하는 대신 작동합니다. for ... else 절은 최종 답변 없이 한도에 도달했을 때만 실행되므로, 이를 통해 모델이 무한 루프에 갇혔는지 감지할 수 있습니다.

프롬프트를 집중적으로 유지하세요. 도구를 호출할 때와 직접 응답할 때를 명시하는 짧은 시스템 메시지는 불필요한 호출을 줄이며, 각 건너뛴 호출은 왕복, 토큰 및 속도 제한 할당량 1회분을 절약합니다.

도구 실행 보호

모델 출력은 신뢰할 수 없는 입력입니다. 모델이 선택한 도구 이름이나 인수는 잘못되거나 이상하거나, 모델이 웹 페이지나 사용자 텍스트에서 읽은 텍스트의 영향을 받을 수 있습니다. 모든 호출을 공개 인터넷에서 도착한 요청처럼 취급하세요.

  • 이름 기반 허용 목록. 당신이 구축한 레지스트리에서 함수를 조회하세요. 문자열을 사용하여 코드를 가져오거나 평가하지 마세요.
  • 읽기와 쓰기를 분리하세요. 읽기 전용 도구는 자유롭게 실행됩니다. 이메일을 보내거나, 돈을 쓰거나, 데이터를 삭제하거나 파일을 수정하는 모든 작업에는 명시적인 인간의 승인이 필요합니다.
  • 인수 검증: 공개한 스키마에 따라 필수 키, 추가 키 없음, 타입 및 길이 제한을 확인합니다.
  • 시간과 출력에 한도 적용. 타임아웃을 적용하고 피드백하기 전에 결과를 잘라내어 방대한 도구 출력이 100,000토큰 컨텍스트 창을 소모하지 않도록 합니다.
  • 반복 및 비용 한도 설정. usage 블록을 사용하여 작업당 단계 수와 세션당 토큰 수를 제한합니다.
  • 모든 호출 로그: 이름, 인수 및 결과를 기록하여 동작을 감사할 수 있습니다.
import json
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutTimeout

ALLOWED = {"ticket_status", "lookup_invoice"}      # read-only tools only
NEEDS_APPROVAL = {"refund_order"}                  # side effects wait for a human
POOL = ThreadPoolExecutor(max_workers=4)

def validate(args: dict, schema: dict) -> None:
    props = schema["properties"]
    for key in schema.get("required", []):
        if key not in args:
            raise ValueError(f"missing argument: {key}")
    for key, val in args.items():
        if key not in props:
            raise ValueError(f"unexpected argument: {key}")
        if props[key]["type"] == "string" and not isinstance(val, str):
            raise ValueError(f"{key} must be a string")
        if isinstance(val, str) and len(val) > 200:
            raise ValueError(f"{key} too long")

def run_tool(name, raw_args, registry, schemas, approve=lambda n, a: False):
    if name in NEEDS_APPROVAL:
        args = json.loads(raw_args)
        if not approve(name, args):
            return "denied: this action needs human approval"
    elif name not in ALLOWED:
        return f"error: tool {name} is not permitted"
    args = json.loads(raw_args)
    validate(args, schemas[name]["parameters"])
    future = POOL.submit(registry[name], **args)
    try:
        return str(future.result(timeout=5))[:2000]    # time and size limits
    except FutTimeout:
        return "error: tool timed out"

도구 결과도 신뢰할 수 없는 것으로 취급하세요. 도구에서 지시사항을 포함하는 스크랩된 텍스트가 반환되면 모델이 이를 따를 수 있습니다. 도구 메시지에서 이러한 내용을 명확한 구분자로 감싸고 시스템 프롬프트에서 도구 출력이 데이터이며 명령어가 아님을 명시하세요.

모델이 사용할 수 있는 도구 스키마 작성

스키마는 모델이 보는 유일한 문서이므로 공개 API와 동일한 주의가 필요합니다. ticket_status 또는 lookup_invoice처럼 작업과 일치하는 동사와 명사로 함수 이름을 지정하고, run 또는 helper과 같은 일반 레이블을 피하세요. description을 지침으로 작성하세요: 함수가 수행하는 작업, 반환 값, 그리고 언제 사용하지 않아야 하는지 명시합니다.

  • 파라미터는 적고 평평하게 유지하세요. 두세 개의 간단한 필드가 10개의 중첩 객체보다 훨씬 더 정확하게 호출됩니다.
  • 고정된 값 목록에는 열거형 사용: 예를 들어 "enum": ["open", "closed"]을 사용하여 모델이 값을 임의로 생성하지 못하게 합니다.
  • 단위나 예시를 포함하여 각 속성 설명: "T-101 같은 티켓 ID"이 일반 문자열보다 낫습니다.
  • 간결한 결과 반환. 문자열이나 작은 JSON 객체가 가장 잘 작동합니다. 긴 덤프는 미미한 이득을 위해 컨텍스트를 소모합니다.
  • 모드 플래그가 있는 포괄적인 도구 하나보다 여러 개의 좁은 도구를 선호하세요.

호출이 유효성 검사를 실패하면, 예외를 발생시키는 대신 유효성 검사 메시지를 도구 결과로 반환하세요. 모델은 일반적으로 다음 턴에서 호출을 수정합니다. 왜 잘못되었는지 읽을 수 있기 때문입니다. 같은 방식으로 두 번 실패하면 루프가 아닌 설명을 수정하세요.

LangChain 구성

LangChain의 ChatOpenAI 클래스는 base_url을 허용하므로 사용자 지정 공급자 패키지가 필요하지 않습니다. model을 uncensored로 설정하고 환경에서 키를 전달하며 bind_tools로 도구를 바인딩합니다. langchain-openai 및 langchain-core을 설치합니다.

import os
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

@tool
def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = ChatOpenAI(
    model="uncensored",
    base_url="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    temperature=0.3,
    max_tokens=400,
)
llm_with_tools = llm.bind_tools([ticket_status])

ai = llm_with_tools.invoke("What is the status of ticket T-101?")
for call in ai.tool_calls:
    print(call["name"], call["args"], "->", ticket_status.invoke(call["args"]))

invoke의 결과는 AIMessage이며, 그 tool_calls 목록에는 파싱된 이름과 인자가 포함되므로 수동 JSON 단계를 건너뛸 수 있습니다. 완전한 자율 루프를 얻으려면 LangChain의 에이전트 생성자에 동일한 llm 객체를 전달하세요. 엔드포인트는 함수 호출을 준수하기만 하면 되며, 그렇게 합니다. 에이전트가 도구를 무시하는 것처럼 보이면 모델이 보는 스키마가 되는 docstring과 타입 힌트가 있는지 확인하세요.

LlamaIndex 구성

LlamaIndex는 채팅 완료 형식을 따르는 서버를 위해 OpenAILike을 제공합니다. 세 가지 설정이 중요합니다. api_base은 API를 가리키고, is_chat_model=True은 채팅 경로를 선택하며, is_function_calling_model=True은 프레임워크가 도구를 전송할 수 있음을 알려줍니다. context_window을 100000으로 설정하여 텍스트 분할기 및 메모리 버퍼가 올바르게 크기를 조정하도록 합니다. llama-index-llms-openai-like을 설치합니다.

import os
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.tools import FunctionTool

def ticket_status(ticket_id: str) -> str:
    """Return the status of a support ticket."""
    return {"T-101": "open", "T-102": "closed"}.get(ticket_id, "unknown ticket")

llm = OpenAILike(
    model="uncensored",
    api_base="https://api.uncensoredapis.com/v1",
    api_key=os.environ["API_KEY"],
    is_chat_model=True,
    is_function_calling_model=True,
    context_window=100000,
    max_tokens=400,
)

tool = FunctionTool.from_defaults(fn=ticket_status)
response = llm.predict_and_call([tool], "Check ticket T-102 for me.")
print(response)

여기서 동일한 llm을 에이전트 워커나 쿼리 엔진에 전달할 수 있습니다. 검색 파이프라인이 하나의 프롬프트에 많은 청크를 넣을 수 있음을 기억하세요. 프롬프트와 응답(completion) 사이에 100,000토큰 창이 공유되므로 검색된 청크 수를 제한하고 max_tokens을 적절히 유지하세요.

예산 내에서 에이전트 운영

에이전트는 하나의 사용자 메시지를 여러 요청으로 변환하므로 용량을 계획해야 합니다. 도구 호출 라운드 5개가 있는 작업은 6번의 모델 호출 비용이 듭니다. 10개의 동시 작업은 짧은 시간 내에 60개의 요청을 생성하며, 이는 채팅 앱이 한도에 도달하기 훨씬 전에 분당 300개 요청의 상한선에 도달하게 합니다. 오류 및 속도 제한 안내서의 토큰 버킷 스로틀링을 클라이언트 앞에 배치하세요.

각 도구 결과가 추가되므로 컨텍스트는 라운드마다 증가합니다. 사용된 도구 출력은 요약하거나 삭제하고, usage.prompt_tokens을 주의 깊게 관찰하세요. 50,000에 접근하면 기록을 압축하세요. 비용은 6번의 호출에 평균 3,000 프롬프트 토큰과 200 응답 토큰이 소요되는 예시 작업을 가정합니다: 이는 18,000 입력 토큰(백만 토큰당 $0.25 기준 약 $0.0045)과 1,200 출력 토큰(백만 토큰당 $1.00 기준 약 $0.0012)입니다. 이 토큰 수는 측정이 아니라 총 비용 계산을 위한 가정입니다. 엔드포인트가 익숙하지 않다면 문서와 무료 체험 크레딧으로 시작하세요.

에이전트 테스트 및 디버깅

에이전트는 단일 함수에 대한 단위 테스트가 놓칠 수 있는 방식으로 실패하므로 대화를 테스트하세요. 모든 실행에 대한 전체 메시지 목록(도구 인자 및 결과 포함)을 기록하고 로그에서 실패를 재생하세요. 세 가지 습관이 빠르게 효과를 냅니다.

  1. 모델 모의 실행. 클라이언트를 스텁 처리하여 스크립트화된 tool_calls 메시지를 반환합니다. 이는 네트워크 및 비용 없이 레지스트리, 검증 및 루프 종료를 테스트합니다.
  2. 샘플링 고정. 도구 기반 작업에는 0.2와 같은 낮은 temperature을 사용하세요. 낮은 무작위성은 더 반복 가능한 인수 JSON을 제공하며, 나중에 쓰기 중심 단계에서 이를 높일 수 있습니다.
  3. 궤적 확인. 고정된 프롬프트 몇 개에 대해 최종 텍스트뿐만 아니라 어떤 도구가 호출되었는지와 그 순서를 확인합니다.

finish_reason도 확인하세요. 도구 호출 중간에 length 값이 나타나면 인자가 잘렸음을 의미합니다. max_tokens을(최대 32,000까지) 높이면 JSON이 완료됩니다. 스트리밍도 도구와 함께 작동하지만, 호출 인덱스별로 인자 조각을 누적하고 스트림이 닫힌 후에만 파싱하세요. 이러한 청크에 대해 복습하려면 참조 문서의 스트리밍 섹션을 참조하세요.

질문과 답변

API는 한 번의 응답에서 병렬 도구 호출을 지원합니까?

메시지에는 tool_calls 배열이 포함되어 있으므로, 해당 배열의 모든 항목을 처리하고 각 call id당 하나의 tool 메시지를 보내야 합니다.

왜 에이전트 루프가 무한히 돌까요?

보통 도구 오류가 명확하지 않거나 시스템 프롬프트에서 언제 중지해야 하는지 명시하지 않기 때문입니다. 단계 수 제한을 추가하고 명확한 오류 문자열을 반환하세요.

LangChain이나 LlamaIndex가 필요합니까?

아니요. OpenAI SDK를 사용한 단순 루프면 충분하며, 프레임워크는 동일한 엔드포인트 위에 오케스트레이션만 추가합니다.

모델이 파괴적인 도구를 실행하는 것을 어떻게 막을 수 있나요?

승인 단계를 거치지 않고 도구를 등록하지 마세요. 등록소에 있는 함수만 실행되므로, 쓰기 작업은 사람의 확인 뒤에 수행되도록 하세요.

키는 양식 하나만 작성하면 받을 수 있습니다

계정을 생성하고 키를 복사한 후 기본 URL을 변경하세요. 이것이 전체 설정입니다.