Ключевые моменты
- Используйте ChatOpenAI, указав в base_url адрес API, а в model — uncensored.
- Конвейеры, потоковая передача, вызов функций и JSON-режим проходят через стандартный маршрут завершения чата.
- Ограничьте параллелизм пакетных запросов до 8 для полного ключа или до 2 для пробного ключа, чтобы избежать ошибок 429.
- Это одна размещенная модель, а не маршрутизатор, поэтому переключение между моделями разных поставщиков отсутствует.
Краткая настройка
Обычный вопрос langchain + llm заключается в том, нужна ли вам специальная интеграция для стороннего эндпоинта. Нет. Обёртка OpenAI chat в LangChain принимает пользовательский базовый URL, а этот API следует формату OpenAI chat completions, поэтому стандартный класс работает. Установите пакет, передайте три значения, и у вас будет объект чат-модели, который можно вставить в любую цепочку.
pip install -U langchain-openai langchain-core
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
temperature=0.7,
)
print(llm.invoke("Give me one sentence about LangChain.").content)
Получите ключ на странице API-ключ. Новые пользователи могут получить $0.50 пробного баланса в один клик после входа, чего хватит, чтобы многократно запустить всё, описанное в этой статье. Идентификатор модели всегда uncensored, запросы отправляются на POST /v1/chat/completions.
Примечание о названии сайта: несмотря на слово "router" в домене, это одна модель с открытыми весами, запущенная независимым поставщиком, а не маршрутизатор между поставщиками и не официальный API какого-либо поставщика. Вы не выбираете между моделями здесь. Ваш код указывает одну модель, и именно она отвечает.
Не храните ключ в коде
Жёсткая привязка ключа работает для первого теста, но становится проблемой при первом же коммите файла. Получайте его из окружения. Библиотеки клиента OpenAI, которые оборачивает LangChain, по умолчанию ищут OPENAI_API_KEY и OPENAI_BASE_URL, поэтому вы можете установить их в оболочке и создать модель с меньшим количеством аргументов.
export OPENAI_API_KEY="YOUR_API_KEY"
export OPENAI_BASE_URL="https://api.nsfwllmrouter.com/v1"
import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url=os.environ.get("OPENAI_BASE_URL", "https://api.nsfwllmrouter.com/v1"),
api_key=os.environ["OPENAI_API_KEY"],
)
Передача базового URL явно, как показано выше, имеет одно преимущество. Если другой проект на той же машине использует настоящий сервис OpenAI и полагается на ту же переменную окружения, ваш явный аргумент предотвратит конфликт.
Первый конвейер с шаблоном промпта
Большая часть реальной работы использует шаблон промпта, модель и парсер вывода, соединенные оператором конвейера. Это стиль языка выражений LangChain, и для пользовательского эндпоинта он не требует ничего особенного.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
prompt = ChatPromptTemplate.from_messages([
("system", "You write short story openings in the style the user asks for."),
("human", "Genre: {genre}\nSetting: {setting}\nWrite three sentences."),
])
chain = prompt | llm | StrOutputParser()
print(chain.invoke({"genre": "noir mystery", "setting": "a rainy harbor town"}))
У модели нет отказов в генерации контента для законного использования взрослыми, что важно, когда ваша цепочка генерирует художественную литературу, сцены ролевой игры или диалоги компаньона, которые более осторожная модель отказалась бы генерировать. Единственное фиксированное правило: сексуальный контент с участием несовершеннолетних никогда не генерируется. Постройте собственную проверку ввода поверх этого, если у вашего продукта более строгие правила.
Потоковая передача и использование
API поддерживает потоковую передачу через серверные события с stream: true, а использование сообщается в конце потока. В LangChain вы вызываете .stream() и перебираете фрагменты.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
for chunk in llm.stream("Write a four-line poem about debugging at night."):
print(chunk.content, end="", flush=True)
Одна деталь, которую следует иметь в виду: потоковый запрос занимает один из ваших слотов параллельности до тех пор, пока поток не закончится. Если ваш интерфейс открывает поток, а пользователь уходит, не закрыв его, этот слот остается занятым. Для полного ключа у вас есть 8 параллельных запросов, поэтому несколько брошенных потоков могут заблокировать новые. Отмените итератор, когда клиент отключится.
Вызов функций и агенты
Вызов инструментов поддерживается: вы передаёте tools и опционально tool_choice, модель отвечает tool_calls (которые также можно потоково передавать), и вы отправляете результаты обратно как сообщения с role: "tool". В LangChain bind_tools обрабатывает формат передачи данных за вас.
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
@tool
def word_count(text: str) -> int:
'Count the words in a piece of text.'
return len(text.split())
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
llm_with_tools = llm.bind_tools([word_count])
msg = llm_with_tools.invoke("How many words are in: the quick brown fox?")
for call in msg.tool_calls:
print(call["name"], call["args"], word_count.invoke(call["args"]))
Для полного цикла агента помощники агента LangChain или LangGraph выполняют этот цикл за вас: вызовите модель, запустите запрошенный инструмент, добавьте сообщение инструмента, вызовите снова. Использование инструмента следует стандартной форме OpenAI, поэтому тот же код, который вы написали бы для любого совместимого эндпоинта, работает. Как и для любой модели с открытыми весами, протестируйте свои конкретные схемы инструментов. Держите имена аргументов простыми, описания короткими, а список инструментов небольшим, так как длинные списки дают более слабым моделям больше возможностей выбрать неверный.
Структурированный вывод с JSON-режимом
JSON-режим доступен через response_format: {"type": "json_object"}. В LangChain вы можете передать его как аргумент модели и распарсить результат самостоятельно, что дает вам контроль над проверкой и повторными попытками.
import json
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
model_kwargs={"response_format": {"type": "json_object"}},
)
msg = llm.invoke(
"Return a JSON object with keys title and tags (a list of three strings) "
"for an article about hiking."
)
data = json.loads(msg.content)
print(data["title"], data["tags"])
Скажите слово "JSON" в промпте и опишите ключи, которые вы хотите. JSON-режим делает вывод действительным JSON, но не гарантирует вашу точную схему, поэтому проверяйте ключи после разбора и повторяйте попытку при сбое. Поскольку неудачные запросы не оплачиваются, цикл повторных попыток для некорректного вывода обходится дешево.
Параллельные запросы, повторные попытки и лимиты
LangChain упрощает распределение задач с помощью .batch(), и именно здесь большинство пользователей сталкиваются со своим первым лимитом запросов. Полный ключ позволяет 8 параллельных запросов и 300 запросов в минуту. Пробный ключ позволяет 2 параллельных запроса и 60 в минуту. Превышение лимита приводит к ответу API с HTTP 429. Код ошибки rate_limited при превышении запросов в минуту, и concurrency при одновременном запуске слишком большого количества запросов. При временной перегрузке вы также получаете 429 с заголовком Retry-After: 5.
Установите параллелизм пакетных запросов самостоятельно и позвольте клиенту повторить попытку с экспоненциальной задержкой:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
max_retries=4,
timeout=120,
)
inputs = [f"Summarize idea number {i} in one line." for i in range(40)]
results = llm.batch(inputs, config={"max_concurrency": 6})
print(len(results))
Значение 6 для полного ключа оставляет запас для других запросов из той же учетной записи. Для пробного ключа используйте 2. Если вы все еще видите ошибки concurrency, ищите потоки, которые никогда не завершались, так как каждый из них занимает слот. Каждый ответ содержит X-Request-Id, который является значением для указания при обращении в поддержку, и X-Balance-USD, который показывает ваш оставшийся баланс.
Асинхронные вызовы для веб-приложений
Внутри веб-сервера вы редко хотите блокирующие вызовы. У каждой модели чата LangChain есть асинхронные аналоги ее методов, называемые ainvoke, astream и abatch, и они работают с этим эндпоинтом так же, как и синхронные версии.
import asyncio
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="uncensored",
base_url="https://api.nsfwllmrouter.com/v1",
api_key="YOUR_API_KEY",
)
async def main():
async for chunk in llm.astream("Describe a quiet harbor at dawn in two sentences."):
print(chunk.content, end="", flush=True)
asyncio.run(main())
В асинхронной службе ограничьте количество активных вызовов семафором, размер которого соответствует вашему ключу, например 8 для полного ключа. Это предотвратит заполнение всех слотов одним занятым эндпоинтом вашего собственного приложения и нехватку слотов у остальных. Перехватывайте исключения вокруг вызова, логируйте заголовок X-Request-Id, когда он у вас есть, и показывайте пользователю короткое сообщение о повторной попытке вместо трассировки стека.
Совет по отладке: если конвейер ведет себя странно, распечатайте точные сообщения, которые получает модель. Шаблоны промптов иногда вставляют лишний пробел или пропускают переменную, и модель может ответить только на то, что ей отправили. Переключатели verbose и debug LangChain или трассировка LangSmith, если вы уже используете его, показывают окончательный список сообщений.
Что означает контекстное окно для конвейеров
Контекстное окно составляет 100 000 токенов для промпта и завершения вместе, а max_tokens является необязательным. Этого достаточно для цепочек извлечения данных, которые помещают множество документов в один промпт, или для памяти разговора, которая хранит длинную историю. Это также достаточно много, чтобы расходы заметно выросли, если вы отправляете полную историю на каждом шаге без сокращения.
Быстрый расчёт. Шаг извлечения, который отправляет 12 000 входных токенов и получает 800 обратно, стоит 0,012 x $0,15 + 0,0008 x $0,50 = $0,0018 + $0,0004 = $0,0022. Запустите это 1 000 раз, и вы заплатите $2,20. Цены составляют $0,15 за 1M входных токенов и $0,50 за 1M выходных токенов, без подписки, поэтому вы платите только за то, что фактически использует цепочка. На странице цены есть варианты пополнения, а в документации быстрого старта перечислены все поддерживаемые параметры. Промпты не используются для обучения.
