Building LLM applications from scratch means reimplementing common patterns: prompt templates, chains, agents, memory. Frameworks like LangChain provide these building blocks, accelerating development. But frameworks also have trade-offs.
Here’s how to use AI application frameworks effectively.
The Framework Landscape
Current Options
ai_frameworks:
langchain:
focus: Comprehensive LLM application framework
strengths: Extensive integrations, large community, many examples
concerns: Complexity, abstraction overhead, rapid changes
llamaindex:
focus: Data ingestion and retrieval for LLMs
strengths: Strong RAG support, structured data handling
concerns: Narrower scope than LangChain
semantic_kernel:
focus: Microsoft's orchestration framework
strengths: Enterprise integration, .NET/Python support
concerns: Smaller community, Microsoft ecosystem focus
haystack:
focus: NLP pipelines and search
strengths: Production-focused, good search integration
concerns: Learning curve, less LLM-focused historically
roll_your_own:
focus: Custom implementation
strengths: Full control, no abstraction overhead
concerns: More code to maintain, reinventing wheels
LangChain Fundamentals
Core Concepts
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema.output_parser import StrOutputParser
# LLM wrapper
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
# Prompt template
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that explains technical concepts."),
("human", "Explain {concept} in simple terms.")
])
# Chain: prompt -> LLM -> output parser
chain = prompt | llm | StrOutputParser()
# Invoke
result = chain.invoke({"concept": "kubernetes"})
Chains
from langchain.chains import LLMChain, SequentialChain
# Simple chain
summarize_chain = LLMChain(
llm=llm,
prompt=ChatPromptTemplate.from_template(
"Summarize this text in 3 bullet points:\n{text}"
),
output_key="summary"
)
# Sequential chain: multiple steps
analyze_chain = LLMChain(
llm=llm,
prompt=ChatPromptTemplate.from_template(
"Based on this summary, what are the key implications?\n{summary}"
),
output_key="analysis"
)
overall_chain = SequentialChain(
chains=[summarize_chain, analyze_chain],
input_variables=["text"],
output_variables=["summary", "analysis"]
)
result = overall_chain({"text": long_document})
# result["summary"], result["analysis"]
RAG with LangChain
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Pinecone
from langchain.chains import RetrievalQA
from langchain.text_splitter import RecursiveCharacterTextSplitter
# Document processing
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200
)
docs = text_splitter.split_documents(documents)
# Vector store
embeddings = OpenAIEmbeddings()
vectorstore = Pinecone.from_documents(docs, embeddings, index_name="my-index")
# RAG chain
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff", # or "map_reduce" for large contexts
retriever=vectorstore.as_retriever(search_kwargs={"k": 5}),
return_source_documents=True
)
result = qa_chain({"query": "How does authentication work?"})
print(result["result"])
print(result["source_documents"])
Agents
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain.tools import Tool
from langchain import hub
# Define tools
def search_database(query: str) -> str:
"""Search the product database."""
# Implementation
return f"Found products matching: {query}"
def get_order_status(order_id: str) -> str:
"""Get status of an order."""
# Implementation
return f"Order {order_id} is shipped"
tools = [
Tool(
name="search_database",
func=search_database,
description="Search for products in the database"
),
Tool(
name="get_order_status",
func=get_order_status,
description="Get the status of an order by order ID"
)
]
# Create agent
prompt = hub.pull("hwchase17/openai-functions-agent")
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
# Run agent
result = agent_executor.invoke({
"input": "What's the status of order 12345 and do you have any blue widgets?"
})
When to Use Frameworks
Framework Benefits
use_framework_when:
rapid_prototyping:
- Testing ideas quickly
- Building demos
- Exploring LLM capabilities
standard_patterns:
- RAG applications
- Chatbots with memory
- Multi-step workflows
integration_heavy:
- Many data sources
- Multiple LLM providers
- Various vector stores
learning:
- Understanding patterns
- Exploring possibilities
- Following tutorials
Roll Your Own When
avoid_framework_when:
simple_use_case:
- Single API call
- Basic prompt template
- Adds unnecessary complexity
performance_critical:
- Framework overhead matters
- Need fine-grained control
- Optimizing latency
production_stability:
- Framework changes frequently
- Abstractions hide issues
- Debugging is harder
specific_requirements:
- Framework doesn't fit well
- Custom patterns needed
- Better to own the code
Framework Pitfalls
Common Issues
framework_pitfalls:
abstraction_hiding:
problem: Don't understand what's happening
symptom: Debug issues are mysterious
solution: Read framework source, understand layers
version_churn:
problem: Rapid breaking changes
symptom: Code breaks on updates
solution: Pin versions, test updates carefully
over_engineering:
problem: Using framework for simple tasks
symptom: 100 lines for what could be 10
solution: Right-size tool to task
cargo_culting:
problem: Copying patterns without understanding
symptom: Code works but you don't know why
solution: Understand before implementing
Keeping It Simple
# Sometimes a simple function is better than a framework
# Instead of LangChain chain for simple task:
def summarize(text: str) -> str:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "Summarize in 3 bullet points."},
{"role": "user", "content": text}
]
)
return response.choices[0].message.content
# vs LangChain equivalent (more setup, same result)
Production Considerations
Observability
from langchain.callbacks import get_openai_callback
from langchain.callbacks.base import BaseCallbackHandler
# Track costs and tokens
with get_openai_callback() as cb:
result = chain.invoke({"question": "What is RAG?"})
print(f"Total tokens: {cb.total_tokens}")
print(f"Total cost: ${cb.total_cost:.4f}")
# Custom callback for logging
class LoggingCallback(BaseCallbackHandler):
def on_llm_start(self, serialized, prompts, **kwargs):
logger.info(f"LLM start: {prompts}")
def on_llm_end(self, response, **kwargs):
logger.info(f"LLM response: {response}")
def on_chain_error(self, error, **kwargs):
logger.error(f"Chain error: {error}")
Testing
# Test chains with mocked LLMs
from langchain.llms.fake import FakeListLLM
def test_summarize_chain():
fake_llm = FakeListLLM(responses=["• Point 1\n• Point 2\n• Point 3"])
chain = create_summarize_chain(llm=fake_llm)
result = chain.invoke({"text": "Some long text..."})
assert "Point 1" in result
Key Takeaways
- Frameworks accelerate development for common patterns
- LangChain: comprehensive but complex; LlamaIndex: focused on RAG
- Use frameworks for prototyping and standard patterns
- Roll your own for simple cases or production stability
- Watch out for abstraction hiding and version churn
- Understand what the framework is doing under the hood
- Add observability for production monitoring
- Test with mocked LLMs for reliability
- Right-size the tool to the task
Frameworks are tools, not requirements. Use them when they help.