n4nAI

Qwen2.5-72B in LangChain: a complete setup guide

Complete guide to integrating Qwen2.5-72B with LangChain — local Ollama setup, OpenAI-compatible endpoints, streaming patterns, and production pitfalls.

n4n Team5 min read1,016 words

Audio narration

Coming soon — every post will get a voice note here.

Qwen2.5-72B has become the default choice for teams needing a strong open-weight model that runs on consumer GPUs or fits comfortably behind an OpenAI-compatible API. This guide walks through the practical paths to get it working in LangChain — from local Ollama to hosted endpoints — with the integration patterns that actually hold up in production.

Why Qwen2.5-72B matters for production

The 72B parameter variant hits a sweet spot: it outperforms most 70B-class models on coding, reasoning, and multilingual benchmarks while fitting on a single 48GB GPU (or two 24GB cards) with 4-bit quantization. Unlike some flagship releases, the Qwen2.5 series ships with a permissive Apache 2.0 license and a tokenizer that handles structured output reliably — critical when you’re building agents that emit JSON or tool calls.

LangChain’s abstraction layer makes swapping providers straightforward, but the devil lives in the details: context window handling, streaming semantics, and how each backend surfaces token usage. We’ll cover the three deployment patterns you’ll actually use.

Prerequisites and model access

Before writing code, decide how you’ll serve the model. Three paths dominate:

Path Best for Hardware Latency
Ollama local Development, air-gapped, cost-sensitive 2×24GB or 1×48GB VRAM ~50-100ms/token
vLLM / TGI self-hosted High-throughput, custom routing 4-8×H100/A100 ~20-40ms/token
OpenAI-compatible gateway Zero-ops, fallback, multi-model None (API) ~100-300ms/token

For local runs, pull the quantized model:

ollama pull qwen2.5:72b-instruct-q4_K_M

The q4_K_M quantization (4-bit K-quant medium) preserves quality while fitting in ~40GB VRAM. If you have 48GB+, try q5_K_M or q6_K for marginally better reasoning.

Running locally with Ollama

Ollama exposes an OpenAI-compatible endpoint at http://localhost:11434/v1. LangChain’s ChatOllama class wraps this directly, but the native integration gives you more control over parameters like num_ctx and num_predict.

from langchain_ollama import ChatOllama

llm = ChatOllama(
    model="qwen2.5:72b-instruct-q4_K_M",
    base_url="http://localhost:11434",
    temperature=0.1,
    num_ctx=32768,        # Qwen2.5 supports 128K; leave headroom
    num_predict=4096,     # max output tokens
    keep_alive="10m",     # keep model loaded between requests
)

Pitfall: Ollama defaults num_ctx to 2048. For any real workload — RAG, long-context summarization, multi-turn agents — you must raise this explicitly. The model was trained with 128K context; truncating to 2K silently degrades performance.

Pitfall: keep_alive defaults to 5 seconds. Without extending it, the model unloads between requests, adding 10-30s cold-start latency. Set to "10m" or -1 (indefinite) for interactive workloads.

Running via OpenAI-compatible endpoints

Most hosted providers (Together, Fireworks, Anyscale, and gateways like n4n.ai) expose /v1/chat/completions with an OpenAI-compatible schema. This lets you use ChatOpenAI with a custom base_url — no vendor-specific SDK required.

from langchain_openai import ChatOpenAI
import os

llm = ChatOpenAI(
    model="qwen2.5-72b-instruct",
    base_url="https://api.n4n.ai/v1",   # or your provider's endpoint
    api_key=os.getenv("N4N_API_KEY"),   # never hardcode
    temperature=0.1,
    max_tokens=4096,
    timeout=60,
    max_retries=3,
)

Tradeoff: You lose access to Ollama-specific parameters (num_ctx, num_gpu_layers, keep_alive). Most gateways enforce their own context limits (often 32K or 64K) and handle model lifecycle server-side. The upside: zero GPU management, automatic fallback when a provider degrades, and per-token metering baked in.

Critical: Verify the provider’s actual context window. Some advertise 128K but truncate silently at 32K. Send a 50K-token prompt and inspect usage.prompt_tokens in the response to confirm.

LangChain integration patterns

Basic invocation

from langchain_core.messages import HumanMessage, SystemMessage

messages = [
    SystemMessage(content="You are a senior Python engineer. Write concise, typed code."),
    HumanMessage(content="Implement a retry decorator with exponential backoff and jitter."),
]

response = llm.invoke(messages)
print(response.content)

Structured output with Pydantic

Qwen2.5 handles JSON mode reliably. Use with_structured_output for type-safe parsing:

from pydantic import BaseModel, Field
from typing import Literal

class CodeReview(BaseModel):
    severity: Literal["low", "medium", "high", "critical"]
    issue: str
    suggestion: str
    line_start: int
    line_end: int

structured_llm = llm.with_structured_output(CodeReview, method="json_mode")

review = structured_llm.invoke([
    HumanMessage(content="Review this code for security issues:\n```python\nimport pickle\ndata = pickle.load(open('user_data.pkl', 'rb'))\n```")
])
print(review.severity, review.issue)

Pitfall: method="json_mode" requires the model to support response_format={"type": "json_object"}. Most OpenAI-compatible endpoints do; Ollama added support in 0.3.x. If your backend rejects it, fall back to method="function_calling" (if tools are supported) or parse manually with a JSON-output prompt.

Tool calling / function calling

Qwen2.5-72B was trained with tool calling. LangChain’s bind_tools works across backends that implement the OpenAI tool schema:

from langchain_core.tools import tool

@tool
def get_weather(location: str, unit: Literal["c", "f"] = "c") -> str:
    """Get current weather for a location."""
    # implementation here
    return f"22°{unit.upper()}, partly cloudy"

llm_with_tools = llm.bind_tools([get_weather])

response = llm_with_tools.invoke("What's the weather in Tokyo?")
print(response.tool_calls)
# [{'name': 'get_weather', 'args': {'location': 'Tokyo', 'unit': 'c'}, 'id': 'call_...'}]

Pitfall: Not all gateways forward tool calls identically. Some strip tool_calls from streaming chunks. Test your specific endpoint before building agent loops around it.

Streaming and async patterns

Streaming is non-negotiable for UX. LangChain’s astream and astream_events work with both local and remote backends.

Token-by-token streaming

async def stream_response(prompt: str):
    async for chunk in llm.astream(prompt):
        print(chunk.content, end="", flush=True)
    print()

Streaming with tool calls (agent loops)

from langchain_core.messages import ToolMessage

async def run_agent(user_input: str):
    messages = [HumanMessage(content=user_input)]
    
    while True:
        # Stream the model's response
        tool_calls = []
        async for chunk in llm_with_tools.astream(messages):
            if chunk.tool_call_chunks:
                tool_calls.extend(chunk.tool_call_chunks)
            elif chunk.content:
                print(chunk.content, end="", flush=True)
        
        if not tool_calls:
            break
        
        # Execute tools and feed results back
        for tc in tool_calls:
            if tc["name"] == "get_weather":
                result = get_weather.invoke(tc["args"])
                messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))
        
        # Continue loop for next model turn

Pitfall: Streaming tool calls arrive as deltas (tool_call_chunks). You must accumulate them until id, name, and arguments are complete before invoking. The pattern above handles this, but it’s easy to invoke prematurely if you only check for tool_call_chunks existence.

Common pitfalls and tradeoffs

Context window mismatch

Backend Advertised Actual (observed)
Ollama (default) 128K 2K unless num_ctx set
Together AI 128K 128K
Fireworks 128K 128K
n4n.ai 128K 128K (forwards provider hints)
vLLM (self-hosted) 128K Configurable via --max-model-len

Always verify with a long prompt. Silent truncation is the most common cause of “the model got stupid” complaints.

Quantization drift

4-bit quantization (Q4_K_M) is the production default, but it shifts probability mass. You may see:

  • Slightly more repetition on long generations
  • Degraded performance on exact-match tasks (regex, specific API signatures)
  • Occasional tokenizer edge cases (rare tokens map differently)

If you hit quality walls, test q5_K_M or q6_K before assuming the model is the problem. The VRAM cost is ~8-12GB more.

Temperature and top-p for code

For code generation, use temperature=0.1 and top_p=0.95. Higher temperatures invite hallucinated APIs; lower temperatures cause repetition. Qwen2.5’s tokenizer has strong code priors — you rarely need top_k tuning.

Rate limits and fallback

Local Ollama has no rate limits but no redundancy. Hosted endpoints enforce RPM/TPM limits. Build retry logic with exponential backoff:

from tenacity import retry, stop_after_attempt, wait_exponential_jitter

@retry(
    wait=wait_exponential_jitter(initial=1, max=30),
    stop=stop_after_attempt(3),
)
async def safe_invoke(messages):
    return await llm.ainvoke(messages)

If you route through a gateway that implements automatic fallback (n4n.ai does this when a provider returns 429 or 5xx), you can skip custom retry logic for those error classes — but keep it for network errors and timeouts.

Production considerations

Observability

Log usage metadata on every call. LangChain callbacks make this straightforward:

from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult

class UsageLogger(BaseCallbackHandler):
    def on_llm_end(self, response: LLMResult, **kwargs):
        usage = response.llm_output.get("token_usage", {})
        print(f"prompt_tokens={usage.get('prompt_tokens')} "
              f"completion_tokens={usage.get('completion_tokens')} "
              f"total_tokens={usage.get('total_tokens')}")

llm = llm.with_config(callbacks=[UsageLogger()])

Note: Ollama doesn’t return token_usage by default. Enable it in ollama serve config or parse eval_count/prompt_eval_count from the streaming response.

Caching

For deterministic workloads (classification, extraction), cache at the application layer. LangChain’s InMemoryCache or RedisCache works with any LLM:

from langchain_core.globals import set_llm_cache
from langchain_community.cache import RedisCache
import redis

set_llm_cache(RedisCache(redis.Redis.from_url(os.getenv("REDIS_URL"))))

Model versioning

Pin the model digest, not just the tag. Ollama tags are mutable:

# Bad: pulls latest, may change behavior
model="qwen2.5:72b-instruct-q4_K_M"

# Good: pins exact digest (get via `ollama show --format '{{.Digest}}' qwen2.5:72b-instruct-q4_K_M`)
model="qwen2.5:72b-instruct-q4_K_M@sha256:abc123..."

Hosted APIs version via model ID (e.g., qwen2.5-72b-instruct-20241201). Pin the dated variant.

Summary checklist

  • Choose deployment: local (Ollama/vLLM) vs. hosted gateway
  • Set num_ctx ≥ 32K for any non-trivial workload
  • Use ChatOpenAI with custom base_url for OpenAI-compatible endpoints
  • Enable keep_alive on Ollama to avoid cold starts
  • Verify actual context window with a long prompt test
  • Use with_structured_output(method="json_mode") for typed responses
  • Stream with astream / astream_events; accumulate tool call deltas
  • Log token usage via callbacks; cache deterministic calls
  • Pin model digests or dated model IDs in production

Qwen2.5-72B is a workhorse. The integration effort is mostly about plumbing — context windows, streaming semantics, and fallback logic — not model capability. Get the plumbing right and the model stays out of your way.

Tagsqwenlangchainsetup-guideopen-source

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All open-source & local models in frameworks (llama 4, mistral, deepseek, qwen) posts →