LangChain
LangChain + usellm - OpenAI-совместимый API для чата, эмбеддингов и RAG через один ключ
LangChain
LangChain через usellm: ChatOpenAI и эмбеддинги, один ключ, оплата в рублях, VPN не нужен.
usellm - OpenAI-совместимый API. Подключайте ChatOpenAI / OpenAIEmbeddings из langchain-openai с base_url="https://api.usellm.ru/v1/".
Не используйте ChatAnthropic, init_chat_model("claude-...") или langchain-openrouter: они уйдут мимо usellm.
Требования
- Python 3.10+
- Аккаунт на usellm и API-ключ
- Установленные пакеты:
pip install -U langchain-openai httpx numpy
httpx нужен для обхода проблемы с эмбеддингами (см. ниже), numpy - для cosine similarity в InMemoryVectorStore.
Чат
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model="zai/glm-5.3-flash",
api_key="***",
base_url="https://api.usellm.ru/v1/",
max_tokens=600,
)
response = llm.invoke("Объясни, что такое LangChain, в трёх предложениях.")
print(response.content)
Имя модели - полный slug из каталога usellm с префиксом провайдера через /. Без префикса (claude-sonnet-5) slug не найдётся в usellm и вернётся ошибка 404.
Для рассуждающих моделей закладывайте max_tokens с запасом: внутренний reasoning_tokens идёт сверх лимита и съедает видимый вывод. Для коротких ответов хватит 600–800, для кода - 1500–2500. Подробнее в Reasoning Tokens.
Эмбеддинги
OpenAIEmbeddings из langchain-openai не работает с usellm as-is. LangChain пре-токенизирует тексты через tiktoken и шлёт {"input": [[15339, 1917]]}: массив целочисленных токенов. usellm принимает только строки или массив строк и возвращает 400 BAD_REQUEST.
Обходной путь - кастомный класс через httpx:
import httpx
from typing import List
from langchain_core.embeddings import Embeddings
class UsellmEmbeddings(Embeddings):
"""Embeddings через прямой httpx — обход LangChain-токенизации."""
def __init__(self, model: str, api_key: str, base_url: str = "https://api.usellm.ru/v1/"):
self.model = model
self.api_key = api_key
self.base_url = base_url.rstrip("/")
def _post(self, input):
r = httpx.post(
f"{self.base_url}/embeddings",
headers={"Authorization": f"Bearer {self.api_key}"},
json={"model": self.model, "input": input},
timeout=30,
)
r.raise_for_status()
return [d["embedding"] for d in r.json()["data"]]
def embed_documents(self, texts):
return self._post(list(texts))
def embed_query(self, text):
return self._post([text])[0]
embeddings = UsellmEmbeddings(
model="alibaba/qwen3.8-flash",
api_key="***",
)
vectors = embeddings.embed_documents([
"Искусственный интеллект",
"Машинное обучение",
"Нейронные сети",
])
print(len(vectors), "векторов,", len(vectors[0]), "измерений")
Примеры
Цепочка
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "Ты — эксперт по {topic}. Отвечай кратко и по делу."),
("user", "{question}"),
])
chain = prompt | llm
response = chain.invoke({
"topic": "Python",
"question": "Какие основные отличия list и tuple?",
})
print(response.content)
Стриминг
for chunk in llm.stream("Напиши короткий рассказ о роботе"):
print(chunk.content, end="", flush=True)
print()
Стриминг отдаёт токены по мере генерации: используйте в UI для отзывчивости.
Async: ainvoke и astream
Для FastAPI / asyncio-приложений LangChain поддерживает async-API:
import asyncio
async def main():
# одиночный запрос
r = await llm.ainvoke("Расскажи про RAG")
print(r.content)
# стриминг
async for chunk in llm.astream("Сгенерируй 5 идей для стартапа"):
print(chunk.content, end="", flush=True)
asyncio.run(main())
Tool calling
usellm проксирует tool calling по OpenAI-схеме. Через LangChain - стандартный bind_tools:
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
"""Возвращает текущую погоду в городе."""
return f"В {city} +18°C, ясно"
llm_with_tools = llm.bind_tools([get_weather])
response = llm_with_tools.invoke("Какая погода в Москве?")
for call in response.tool_calls:
print(f"tool: {call['name']}, args: {call['args']}")
# tool: get_weather, args: {'city': 'Москва'}
# → дальше вызываете функцию и передаёте результат обратно в llm
Полный цикл с ToolNode и агентом - в документации LangGraph.
Structured output
Если нужно получить не строку, а dict по схеме:
from pydantic import BaseModel
class Review(BaseModel):
sentiment: str # positive | neutral | negative
score: int # 1..10
summary: str
structured = llm.with_structured_output(Review)
r: Review = structured.invoke("Отзыв: 'Доставка быстрая, но курьер был груб'")
print(r.sentiment, r.score, r.summary)
RAG: короткий (5 строк без LangChain vectorstore)
Для простых случаев - httpx для эмбеддингов, numpy для cosine similarity, без LangChain-обёрток:
import httpx
import numpy as np
DOCS = [
"usellm — единый OpenAI-совместимый API к LLM.",
"Оплата в usellm — pay-as-you-go, в рублях.",
"usellm поддерживает chat, embeddings, images, audio и video.",
]
def embed(texts):
r = httpx.post(
"https://api.usellm.ru/v1/embeddings",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"model": "alibaba/qwen3.8-flash", "input": texts},
)
r.raise_for_status()
return [d["embedding"] for d in r.json()["data"]]
doc_vecs = embed(DOCS)
query_vec = embed(["Как работает оплата?"])[0]
sims = np.dot(doc_vecs, query_vec) / (
np.linalg.norm(doc_vecs, axis=1) * np.linalg.norm(query_vec)
)
top = np.argsort(sims)[::-1][:2]
context = "\n".join(f"- {DOCS[i]}" for i in top)
answer = llm.invoke(
f"Контекст:\n{context}\n\nВопрос: Как работает оплата?\n\nОтветь кратко:"
)
print(answer.content)
RAG: полный (InMemoryVectorStore)
Для продакшена - InMemoryVectorStore + кастомные embeddings. Тот же UsellmEmbeddings подставляется в LangChain, тот же pipeline работает:
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
vs = InMemoryVectorStore.from_texts(
[
"usellm — единый OpenAI-совместимый API к LLM.",
"Оплата в usellm — pay-as-you-go, в рублях.",
"usellm поддерживает chat, embeddings, images, audio и video.",
"В LangChain подключение идёт через ChatOpenAI с base_url=https://api.usellm.ru/v1/.",
],
embedding=embeddings, # UsellmEmbeddings сверху
)
retriever = vs.as_retriever()
prompt = ChatPromptTemplate.from_template("Контекст: {context}\n\nВопрос: {question}\n\nОтвет:")
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
print(chain.invoke("Как подключить LangChain?"))
InMemoryVectorStore держит векторы в памяти процесса: подходит для прототипов и небольших датасетов (до ~10k документов). Для продакшена с persistent storage используйте FAISS, Chroma или pgvector (через langchain-community / langchain-postgres).
RAG с длинными документами (text splitter)
Для реальных документов разбивайте текст на чанки перед индексацией:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
chunks = splitter.split_text(long_document_text)
vs = InMemoryVectorStore.from_texts(chunks, embedding=embeddings)
Две модели: fast classify → smart answer
Шаблон «дешёвая модель классифицирует → дорогая генерирует»:
from langchain_openai import ChatOpenAI
fast = ChatOpenAI(
model="alibaba/qwen3.8-flash",
api_key="***",
base_url="https://api.usellm.ru/v1/",
max_tokens=50,
)
smart = ChatOpenAI(
model="anthropic/claude-sonnet-5",
api_key="***",
base_url="https://api.usellm.ru/v1/",
max_tokens=400,
)
classification = fast.invoke(
"Классифицируй: 'Хочу вернуть товар' → жалоба/вопрос/благодарность. Одно слово."
)
response = smart.invoke(f"Напиши вежливый ответ клиенту: {classification.content}")
print(response.content)
Qwen3.8 Flash стоит дёшево и быстро классифицирует, Claude Sonnet 5 генерирует финальный ответ: на длинном потоке экономия ×5–10×.
Специфика usellm
- Стоимость в рублях. Каждый ответ содержит
usage.cost_rub- стоимость запроса в рублях с точностью до 8 знаков. Можно логировать для биллинга.r = llm.invoke("...") print(f"cost: {r.usage_metadata.get('cost_rub', 0):.6f}₽") print(f"tokens: in={r.usage_metadata['input_tokens']}, out={r.usage_metadata['output_tokens']}") - Reasoning tokens. Для рассуждающих моделей в
usage.completion_tokens_details.reasoning_tokensприходит число «мыслей» модели. Они тарифицируются по обычной цене output-токенов, но не видны вresponse.content, и при этом считаются вmax_tokens. Закладывайте запас. - Один ключ на всё.
ChatOpenAI, эмбеддинги (через обёртку), картинки, аудио: один API-ключ, один аккаунт. - Кеширование. usellm проксирует cache-aware провайдеров (Anthropic, OpenAI): при повторных запросах
usage.prompt_tokens_details.cached_tokensпокажет, сколько токенов взято из кеша. Подробнее в Кеширование запросов.
Практика
- Не дублируйте ключ в коде. Используйте переменные окружения:
import os llm = ChatOpenAI( model="anthropic/claude-sonnet-5", api_key=os.environ["USELLM_API_KEY"], base_url="https://api.usellm.ru/v1/", )
Или ещё проще: задайтеOPENAI_API_KEYиOPENAI_BASE_URL=https://api.usellm.ru/v1/, тогдаapi_keyиbase_urlв конструкторе можно опустить (LangChain читает их автоматически). - Slug модели со слешем.
anthropic/claude-sonnet-5- правильно.claude-sonnet-5без префикса вернёт 404. max_tokensподбирайте по задаче. Для классификации хватит 50, для генерации писем - 400–800, для кода - 1500–2500, для рассуждающих моделей - с запасом ×2.- LangSmith tracing. Для отладки промптов и токенов:
export LANGSMITH_TRACING=true export LANGSMITH_API_KEY=lsv2_*** export LANGSMITH_PROJECT=usellm-langchain
Все вызовыllm.invoke/chain.invokeначнут логироваться в smith.langchain.com.