Local Models
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page was written against Python 3.12 with langchain 1.4.3, langchain-core 1.6.5, langchain-ollama 1.1.0,
langchain-openai 1.6.6 and langchain-huggingface 1.2.2 (with the ollama client 0.6.3 and openai 3.20.0 that
they install), all checked on 2026-09-29. No Ollama, vLLM or LM Studio server was available while writing it, so every
model call below was executed against a small stub HTTP server that speaks Ollama’s /api/chat, /api/embed,
/api/show and OpenAI’s /v1/chat/completions, /v1/embeddings on localhost. The stub replies with canned text and
a canned tool call: the output shown proves that LangChain builds the right requests and parses the responses, not
that any model answers well. The real models are the AI section’s baseline — llama3.1:8b and nomic-embed-text
on Ollama for DocsAssistant. The Hugging Face classes were only constructed (with a placeholder token in the
environment), never called.
Running a model locally changes three things for a LangChain application, and none of them is the API: the model is smaller and follows tool schemas less reliably, the server enforces limits (context length, tool support) that hosted providers hide, and there is usually no bill. Choosing hardware, quantization and servers is the job of Running LLMs Locally; this page is about the LangChain side of the connection — which class talks to which server, what to configure, and how to find out early that a model cannot do what the agent needs.
chat_model() / embeddings()"] -->|LLM_BACKEND=ollama| CO["ChatOllama
OllamaEmbeddings"] DA -->|LLM_BACKEND=openai| CP["ChatOpenAI(base_url=...)
OpenAIEmbeddings(base_url=...)"] CO --> OL["Ollama
:11434 /api/chat"] CP --> VL["vLLM :8000/v1"] CP --> LM["LM Studio :1234/v1"] CP --> DM["Docker Model Runner
:12434/engines/v1"] HF["ChatHuggingFace"] --> EP["HuggingFaceEndpoint
(Inference Providers)"] HF --> PL["HuggingFacePipeline
(in-process transformers)"]
ChatOllama and OllamaEmbeddings
Ollama is the default local backend for DocsAssistant. Its LangChain classes live in the langchain-ollama package
(pip install -U langchain-ollama), which talks to the server’s native REST API rather than its OpenAI-compatible
one — the native API is what exposes Ollama-specific options such as num_ctx and keep_alive. Pull the model
first and use the same tag in model=; the server side is covered in Ollama.
ChatOllama and OllamaEmbeddings against a server (here the stub)from langchain_ollama import ChatOllama, OllamaEmbeddings
llm = ChatOllama(
model="llama3.1:8b",
base_url="http://localhost:11434", # the default; stated here to show where it is set
num_ctx=16384, # context window requested per call
temperature=0,
)
reply = llm.invoke("What is HNSW?")
print(reply.content)
print(reply.usage_metadata)
embeddings = OllamaEmbeddings(model="nomic-embed-text", base_url="http://localhost:11434")
print(len(embeddings.embed_query("hnsw")))
print(len(embeddings.embed_documents(["a", "b"])))
num_ctx value was checked in the request the stub received)HNSW builds a layered graph.
{'input_tokens': 12, 'output_tokens': 7, 'total_tokens': 19}
768
2
ChatOllama returns a normal AIMessage with usage_metadata filled from the server’s token counts, so the
accounting from Chat Models & Messages works unchanged.
OllamaEmbeddings implements the Embeddings interface (embed_query, embed_documents), which is what the pgvector
store from PostgreSQL & pgvector and the retrievers on
Retrieval & the RAG Bridge take. The two parameters worth setting
deliberately:
-
num_ctx— Ollama loads models with a small default context and silently truncates longer prompts. Agents accumulate tool results quickly, so set it explicitly and see Ollama’s context-length pitfall. -
validate_model_on_init=True— checks at construction that the model has been pulled, so a missing model fails at start-up instead of on the first user request.
from langchain_ollama import ChatOllama
try:
ChatOllama(model="llama3.1:70b", base_url="http://localhost:11434", validate_model_on_init=True)
except Exception as e:
print(type(e).__name__, str(e)[:110])
ChatOllama(model="llama3.1:8b", base_url="http://localhost:11434", validate_model_on_init=True)
print("ok")
llama3.1:8b)ValidationError 1 validation error for ChatOllama
Value error, Model `llama3.1:70b` not found in Ollama. Please pull the mod
ok
Embedding models are tied to the vectors already stored: nomic-embed-text produces 768 dimensions, the size of the
vector(768) column in DocsAssistant’s store. Swapping in another embedding model, locally or hosted, means
re-embedding the corpus. The EMBEDDING_DIMENSIONS check in the DocsAssistant module below turns that into a start-up check. The integration
pages are ChatOllama and
OllamaEmbeddings; init_chat_model reaches the
same class with model_provider="ollama".
OpenAI-compatible servers with base_url
vLLM, LM Studio and Docker Model Runner all serve the OpenAI Chat Completions shape under a /v1-style prefix, so one
class — ChatOpenAI from langchain-openai — reaches all of them by changing base_url. Only the URL and the model
name differ:
| Server | base_url |
Notes |
|---|---|---|
vLLM |
|
|
LM Studio |
Authentication is off by default; enable Require Authentication before binding beyond loopback. See Desktop Runners. |
|
Docker Model Runner |
Host TCP access is opt-in on Docker Desktop and the API is unauthenticated by design. See Desktop Runners. |
ChatOpenAI pointed at a local server, with a tool boundimport os
from langchain.tools import tool
from langchain_openai import ChatOpenAI
@tool
def docs_search(query: str) -> str:
"""Search the docs."""
return "modules/ROOT/pages/database/vector-rag/ann-indexes.adoc"
llm = ChatOpenAI(
model="meta-llama/Llama-3.1-8B-Instruct", # the name the server reports, not an OpenAI model
base_url="http://localhost:8000/v1",
api_key=os.environ.get("LLM_API_KEY", "not-needed"), # the SDK insists on a non-empty key
)
print(llm.invoke("hi").content)
print(llm.bind_tools([docs_search]).invoke("find hnsw").tool_calls)
Authorization: Bearer not-needed)HNSW builds a layered graph.
[{'name': 'docs_search', 'args': {'query': 'hnsw index'}, 'id': 'call_1', 'type': 'tool_call'}]
Without api_key (or OPENAI_API_KEY) the client refuses to start with a "Missing credentials" error even when the
server needs none, hence the placeholder. The base URL can also come from the environment: LangChain resolves an
explicit base_url first, then OPENAI_API_BASE, then OPENAI_BASE_URL, which the openai SDK reads.
init_chat_model("meta-llama/Llama-3.1-8B-Instruct", model_provider="openai", base_url=…, api_key=…) builds the same
object.
Two limits of this route come straight from the LangChain documentation. ChatOpenAI targets the official OpenAI
specification only: non-standard response fields that servers add, such as reasoning_content, are neither extracted
nor preserved, so a reasoning model’s thinking trace disappears when it is served through vLLM and read with
ChatOpenAI. And features such as tool calling, multimodal input and token streaming "depend on the hosted model", as
the vLLM integration page puts it. Where a dedicated provider package exists — langchain-ollama for Ollama — prefer it over the compatibility endpoint.
Embeddings need one more flag. OpenAIEmbeddings by default tokenizes each text with tiktoken and sends token IDs,
which a local embedding server that expects strings will misread:
check_embedding_ctx_length=False matters for local serversfrom langchain_openai import OpenAIEmbeddings
base = "http://localhost:8000/v1"
OpenAIEmbeddings(model="nomic-embed-text", base_url=base, api_key="x").embed_query("hnsw")
# request body input: [[25105, 2332]] (token ids)
OpenAIEmbeddings(model="nomic-embed-text", base_url=base, api_key="x",
check_embedding_ctx_length=False).embed_query("hnsw")
# request body input: ['hnsw'] (plain text)
The two comments are what the stub recorded as the request body for each call. See the ChatOpenAI, vLLM and Chat Completions API compatibility pages, and LLM Gateways for putting one authenticated endpoint in front of several backends.
One DocsAssistant, several backends
DocsAssistant should not care which server answers. Two functions read the environment and return a chat model and an embedding model; everything else in the application (tools, agent, retriever) receives them as arguments.
docsassistant/models.py: backend chosen by environment variables, no secrets in codeimport os
from langchain_core.embeddings import Embeddings
from langchain_core.language_models import BaseChatModel
from langchain_ollama import ChatOllama, OllamaEmbeddings
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
EMBEDDING_DIMENSIONS = 768 # the vector(768) column of the pgvector store
def chat_model() -> BaseChatModel:
if os.environ.get("LLM_BACKEND", "ollama") == "ollama":
return ChatOllama(
model=os.environ.get("LLM_MODEL", "llama3.1:8b"),
base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"),
num_ctx=16384, # Ollama's default context is much smaller
temperature=0,
validate_model_on_init=True,
)
# vLLM, LM Studio, Docker Model Runner ...: any OpenAI-compatible endpoint
return ChatOpenAI(
model=os.environ["LLM_MODEL"],
base_url=os.environ["LLM_BASE_URL"],
api_key=os.environ.get("LLM_API_KEY", "not-needed"),
temperature=0,
)
def embeddings() -> Embeddings:
if os.environ.get("LLM_BACKEND", "ollama") == "ollama":
emb: Embeddings = OllamaEmbeddings(
model="nomic-embed-text",
base_url=os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434"),
)
else:
emb = OpenAIEmbeddings(
model=os.environ.get("EMBEDDING_MODEL", "nomic-embed-text"),
base_url=os.environ["LLM_BASE_URL"],
api_key=os.environ.get("LLM_API_KEY", "not-needed"),
check_embedding_ctx_length=False, # send text, not tiktoken ids
)
size = len(emb.embed_query("dimension probe"))
if size != EMBEDDING_DIMENSIONS:
raise RuntimeError(f"embedding model returns {size} dimensions, the store expects {EMBEDDING_DIMENSIONS}")
return emb
The module was run in both modes against the stub (OLLAMA_BASE_URL set for the first, LLM_BACKEND=openai,
LLM_MODEL and LLM_BASE_URL for the second) and returned a ChatOllama with num_ctx=16384 and a ChatOpenAI
whose openai_api_base was the given URL; in both cases embeddings() returned 768-dimension vectors and passed the
check. The dimension probe costs one embedding call at start-up and turns the silent failure of a mismatched
embedding model (the store would reject inserts, or worse, retrieval would quietly degrade) into an immediate error.
Hugging Face models with ChatHuggingFace
langchain-huggingface wraps a Hugging Face LLM object in a chat model. ChatHuggingFace takes an llm= that is
either a HuggingFaceEndpoint, which calls a hosted model through Hugging Face’s Inference Providers, or a
HuggingFacePipeline, which loads a transformers model into the Python process itself. The endpoint variant is not
local in the sense of this sub-section — the weights run on Hugging Face’s or a provider’s infrastructure — but it
is how open-weight models are reached without installing anything else. The Hugging Face ecosystem itself
(the Hub, transformers, Inference Providers) has a planned sub-section of its own; here it is only a LangChain
provider.
ChatHuggingFace over an endpoint; the token comes from the environmentimport os
# HUGGINGFACEHUB_API_TOKEN must be set in the environment (a placeholder was used to construct this)
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
llm = HuggingFaceEndpoint(
repo_id="meta-llama/Llama-3.1-8B-Instruct",
task="text-generation",
max_new_tokens=512,
provider="auto", # let Hugging Face choose an inference provider
)
chat = ChatHuggingFace(llm=llm)
print(type(chat).__name__, chat.model_id)
print(hasattr(chat, "bind_tools"))
ChatHuggingFace meta-llama/Llama-3.1-8B-Instruct
True
For in-process inference, HuggingFacePipeline.from_model_id(model_id=…, task="text-generation",
pipeline_kwargs=dict(max_new_tokens=512)) builds the wrapped LLM instead, and a bitsandbytes quantization config
can be passed through model_kwargs. It needs transformers (plus torch, and bitsandbytes/accelerate for
quantization), which were not installed in the environment used here, so that variant is taken from the
ChatHuggingFace integration page and was not run.
Loading a multi-gigabyte model into the application process ties its memory, start-up time and GPU to the web
worker; for anything but experiments, serve the model with a real server from
Running LLMs Locally and use one of the classes above. The integration page’s own model
table lists ChatHuggingFace without token-level streaming, unlike ChatOllama.
Tool-calling caveats for local models
An agent built with create_agent (see Agents with create_agent)
needs the model to emit structured tool calls. That is a property of the model and its chat template, not of
LangChain, and local models are where it goes wrong. The concepts are in
Structured Output & Tool Calling and
Tool Design; these are the local-specific points:
-
Not every model can call tools. Ollama reports each model’s features; only models whose template supports tools accept a tool list, and the library marks them with a tools tag. Sending tools to one that does not fails with an HTTP 400.
-
Servers may need to be told how to parse tool calls. vLLM needs
--enable-auto-tool-choiceand a--tool-call-parsermatching the model family; without them the server does not turn the model’s output into the OpenAI-styletool_callsa client expects. -
Small models are unreliable callers. An 8B model given ten tools picks the wrong one, or malformed arguments, far more often than a frontier model. Keep tool lists short and schemas simple (see Tool Design), and evaluate on your own tasks (Evaluation) instead of trusting a leaderboard.
-
Context is small by default. Tool results fill a small window fast; raise
num_ctx(Ollama) or the server’s context size. -
Non-OpenAI fields are dropped. Reasoning traces and other extensions served on a compatible endpoint do not survive
ChatOpenAI, as above.
The failure is easy to see and easy to detect ahead of time. With a model that lacks the tools capability the error
arrives as an ollama.ResponseError:
from ollama import ResponseError
from langchain_ollama import ChatOllama
small = ChatOllama(model="gemma2:2b", base_url="http://localhost:11434")
try:
small.bind_tools([docs_search]).invoke("find hnsw")
except ResponseError as e:
print(type(e).__name__, e.status_code, e.error)
ResponseError 400 registry.ollama.ai/library/gemma2:2b does not support tools
The stub was scripted to return that message for the gemma2 name; the text a real server sends is Ollama’s, and the
shape (a 400 with an error string) is what to handle (see Ollama — Tool calling). To find out before the first request, ask the server what the
model can do — Ollama’s show-model-details endpoint returns a capabilities list, and tools is the entry for tool
support:
import ollama
def supports_tools(model: str, host: str = "http://localhost:11434") -> bool:
"""True when Ollama lists `tools` among the model's capabilities."""
return "tools" in (ollama.Client(host=host).show(model).capabilities or [])
Against the stub, supports_tools("llama3.1:8b", …) returned True and supports_tools("gemma2:2b", …) returned
False. The capability names are the ones defined in Ollama’s source (completion, tools, vision, thinking,
and others); the API reference documents the capabilities
field.
When a model can not call tools but must still produce structured data, use structured output instead of tool
calling. ChatOllama.with_structured_output defaults to method="json_schema", which sends the schema in Ollama’s
format field (constrained generation) and no tools at all:
from pydantic import BaseModel
from langchain_ollama import ChatOllama
class Route(BaseModel):
page: str
confidence: float
small = ChatOllama(model="gemma2:2b", base_url="http://localhost:11434")
print(small.with_structured_output(Route).invoke("Which page covers HNSW?"))
page='vector-rag/ann-indexes' confidence=0.9
The request the stub received had an empty tools list and the schema’s properties (page, confidence) in format.
The alternative methods are "function_calling" (which uses the tool API and so needs a capable model) and
"json_mode" (valid JSON but no schema enforcement). Strategies for agents that produce structured final answers,
including ToolStrategy, are on Structured Output; the
ChatOllama integration page lists the structured-output
support of the class.
Finally, a capable local model plugs into create_agent exactly like a hosted one. Both backends, run against the stub
with a docs_search tool, produced the same message sequence:
ChatOllama and on ChatOpenAI(base_url=…)from langchain.agents import create_agent
for llm in (ChatOllama(model="llama3.1:8b", base_url=OLLAMA_URL),
ChatOpenAI(model="llama3.1:8b", base_url=OPENAI_URL, api_key="not-needed")):
agent = create_agent(llm, tools=[docs_search])
out = agent.invoke({"messages": [("user", "Which page covers HNSW?")]})
print(type(llm).__name__, [type(m).__name__ for m in out["messages"]])
ChatOllama ['HumanMessage', 'AIMessage', 'ToolMessage', 'AIMessage']
ChatOpenAI ['HumanMessage', 'AIMessage', 'ToolMessage', 'AIMessage']
(OLLAMA_URL and OPENAI_URL stand for the two base URLs; the stub replied with a tool call to the first request and
with plain text once a tool message was present. See LangChain — Agents.) A model that cannot do this reliably is a signal to route the
tool-using steps to a hosted model with ModelFallbackMiddleware or a model-selection middleware, as described on
Middleware, while keeping embeddings and simple chat local. Data-sensitivity rules
should decide that routing, not convenience — see Guardrails & Security and Agent Guardrails & Safety.
Where local models fit
Local models earn their place in the DocsAssistant stack for embeddings (cheap, high-volume, private), for development and tests (no network, no bill), and for privacy-bound workloads. The AI section’s other pages cover the surrounding decisions: Open-Weight Models for which weights to run, Hardware Sizing for what they need, and Running Local LLMs in Production for operating them. Tracing a local model with LangSmith works like any other provider (Observability with LangSmith); the same MCP tools attach to it too (MCP Adapters).