Migrating from LangChain to SteadyText¶
Simplify your AI stack by replacing complex LangChain abstractions with SteadyText's straightforward, deterministic approach.
Why Migrate from LangChain?¶
LangChain | SteadyText |
---|---|
Complex abstractions | Simple, direct API |
Multiple dependencies | Single lightweight library |
Non-deterministic chains | 100% deterministic outputs |
Verbose configuration | Zero configuration |
External LLM costs | Free local execution |
Debugging nightmares | Predictable behavior |
Quick Comparison¶
Text Generation¶
Before (LangChain):
from langchain import PromptTemplate, LLMChain
from langchain.llms import OpenAI
from langchain.callbacks import get_openai_callback
llm = OpenAI(temperature=0, api_key="sk-...")
template = """Summarize the following text:
{text}
Summary:"""
prompt = PromptTemplate(template=template, input_variables=["text"])
chain = LLMChain(llm=llm, prompt=prompt)
with get_openai_callback() as cb:
summary = chain.run(text="Long text here...")
print(f"Cost: ${cb.total_cost}")
After (SteadyText):
import steadytext
summary = steadytext.generate(f"Summarize the following text: {text}")
# No chains, no templates, no callbacks, no costs!
Embeddings and Vector Stores¶
Before (LangChain):
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import FAISS
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
# Complex setup
loader = TextLoader("document.txt")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
docs = text_splitter.split_documents(documents)
embeddings = OpenAIEmbeddings(api_key="sk-...")
db = FAISS.from_documents(docs, embeddings)
# Search
query = "What is the main topic?"
docs = db.similarity_search(query, k=4)
After (SteadyText + PostgreSQL):
-- Everything in SQL!
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
content TEXT,
embedding vector(1024)
);
-- Load and embed documents
INSERT INTO documents (content, embedding)
SELECT
content,
steadytext_embed(content)::vector
FROM load_text_file('document.txt');
-- Search
SELECT content, 1 - (embedding <=> steadytext_embed('What is the main topic?')::vector) AS score
FROM documents
ORDER BY embedding <=> steadytext_embed('What is the main topic?')::vector
LIMIT 4;
Common LangChain Pattern Migrations¶
1. Prompt Templates → Direct Formatting¶
Before:
from langchain import PromptTemplate, FewShotPromptTemplate
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="Input: {input}\nOutput: {output}"
)
examples = [
{"input": "2+2", "output": "4"},
{"input": "3+3", "output": "6"}
]
few_shot_prompt = FewShotPromptTemplate(
examples=examples,
example_prompt=example_prompt,
prefix="Solve math problems:",
suffix="Input: {input}\nOutput:",
input_variables=["input"]
)
chain = LLMChain(llm=llm, prompt=few_shot_prompt)
result = chain.run(input="5+5")
After:
def solve_math(problem):
prompt = """Solve math problems:
Input: 2+2
Output: 4
Input: 3+3
Output: 6
Input: {problem}
Output:""".format(problem=problem)
return steadytext.generate(prompt, max_tokens=10)
result = solve_math("5+5") # Deterministic: always "10"
2. Chains → Simple Functions¶
Before:
from langchain.chains import SimpleSequentialChain
# First chain: summarize
summarize_chain = LLMChain(llm=llm, prompt=summarize_prompt)
# Second chain: translate
translate_chain = LLMChain(llm=llm, prompt=translate_prompt)
# Combine chains
overall_chain = SimpleSequentialChain(
chains=[summarize_chain, translate_chain],
verbose=True
)
result = overall_chain.run(long_text)
After:
def summarize_and_translate(text, target_lang="Spanish"):
# Step 1: Summarize
summary = steadytext.generate(f"Summarize: {text}", max_tokens=100)
# Step 2: Translate
translation = steadytext.generate(
f"Translate to {target_lang}: {summary}",
max_tokens=150
)
return translation
result = summarize_and_translate(long_text)
3. Agents → Direct Logic¶
Before:
from langchain.agents import load_tools, initialize_agent, AgentType
tools = load_tools(["serpapi", "llm-math"], llm=llm)
agent = initialize_agent(
tools,
llm,
agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
verbose=True
)
result = agent.run("What is the weather in NYC and what is 234 * 432?")
After:
def answer_question(question):
# Determine what's needed
needs = steadytext.generate_json(
f"What tools are needed for: {question}",
schema={
"needs_search": {"type": "boolean"},
"needs_calculation": {"type": "boolean"},
"calculation": {"type": "string"}
}
)
results = []
if needs["needs_calculation"]:
# Direct calculation
calc_result = eval(needs["calculation"]) # In production, use safe eval
results.append(f"Calculation: {calc_result}")
if needs["needs_search"]:
# Your search logic here
results.append("Search: [Results]")
return steadytext.generate(
f"Answer based on: {results}\nQuestion: {question}"
)
4. Document QA → SQL Queries¶
Before:
from langchain.chains import RetrievalQA
from langchain.document_loaders import DirectoryLoader
from langchain.indexes import VectorstoreIndexCreator
loader = DirectoryLoader('docs/', glob="*.txt")
index = VectorstoreIndexCreator(
embedding=OpenAIEmbeddings(),
text_splitter=CharacterTextSplitter(chunk_size=1000)
).from_loaders([loader])
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=index.vectorstore.as_retriever()
)
answer = qa.run("What is the main topic?")
After:
-- Create document chunks table
CREATE TABLE document_chunks (
id SERIAL PRIMARY KEY,
document_name VARCHAR(255),
chunk_number INTEGER,
content TEXT,
embedding vector(1024)
);
-- Load and chunk documents
INSERT INTO document_chunks (document_name, chunk_number, content, embedding)
SELECT
filename,
chunk_number,
chunk_text,
steadytext_embed(chunk_text)::vector
FROM chunk_documents('docs/*.txt', 1000);
-- Question answering function
CREATE FUNCTION answer_question(question TEXT)
RETURNS TEXT AS $$
DECLARE
context TEXT;
BEGIN
-- Get relevant chunks
SELECT string_agg(content, E'\n\n') INTO context
FROM (
SELECT content
FROM document_chunks
WHERE embedding <=> steadytext_embed(question)::vector < 0.3
ORDER BY embedding <=> steadytext_embed(question)::vector
LIMIT 4
) relevant_chunks;
-- Generate answer
RETURN steadytext_generate(
format('Based on this context: %s\n\nAnswer: %s',
context, question)
);
END;
$$ LANGUAGE plpgsql;
5. Output Parsers → Structured Generation¶
Before:
from langchain.output_parsers import PydanticOutputParser, OutputFixingParser
from pydantic import BaseModel, Field
class Person(BaseModel):
name: str = Field(description="person's name")
age: int = Field(description="person's age")
parser = PydanticOutputParser(pydantic_object=Person)
fixing_parser = OutputFixingParser.from_llm(parser=parser, llm=llm)
prompt = PromptTemplate(
template="Extract person info:\n{format_instructions}\n{text}",
input_variables=["text"],
partial_variables={"format_instructions": parser.get_format_instructions()}
)
chain = LLMChain(llm=llm, prompt=prompt)
output = chain.run(text="John is 30 years old")
person = fixing_parser.parse(output) # Might fail and retry!
After:
from pydantic import BaseModel
class Person(BaseModel):
name: str
age: int
# Guaranteed to return valid Person object!
result = steadytext.generate("Extract: John is 30 years old", schema=Person)
# Parses automatically from: "...<json-output>{"name": "John", "age": 30}</json-output>"
Memory and State Management¶
Conversation Memory¶
Before (LangChain):
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
memory = ConversationBufferMemory()
conversation = ConversationChain(
llm=llm,
memory=memory,
verbose=True
)
response1 = conversation.predict(input="Hi, my name is John")
response2 = conversation.predict(input="What's my name?")
After (Simple Python):
class SimpleConversation:
def __init__(self):
self.history = []
def chat(self, user_input):
# Build context from history
context = "\n".join([
f"User: {h['user']}\nAssistant: {h['assistant']}"
for h in self.history[-5:] # Keep last 5 turns
])
prompt = f"{context}\nUser: {user_input}\nAssistant:"
response = steadytext.generate(prompt, max_tokens=100)
# Save to history
self.history.append({
"user": user_input,
"assistant": response
})
return response
conv = SimpleConversation()
response1 = conv.chat("Hi, my name is John")
response2 = conv.chat("What's my name?") # Will remember "John"
Cost and Performance Comparison¶
LangChain + OpenAI Costs¶
# Typical LangChain application
def calculate_langchain_costs(daily_requests):
# Multiple LLM calls per request due to chains
avg_calls_per_request = 3 # Chain steps
tokens_per_call = 500
cost_per_1k_tokens = 0.002 # GPT-3.5
daily_cost = (daily_requests * avg_calls_per_request *
tokens_per_call / 1000 * cost_per_1k_tokens)
print(f"Daily: ${daily_cost:.2f}")
print(f"Monthly: ${daily_cost * 30:.2f}")
print(f"Yearly: ${daily_cost * 365:.2f}")
SteadyText Costs¶
# SteadyText: Always $0 after installation
print("Daily: $0")
print("Monthly: $0")
print("Yearly: $0")
print("Plus: 100x faster, 100% deterministic!")
Testing Strategies¶
Making Tests Deterministic¶
Before (LangChain - Flaky):
def test_qa_chain():
# This test might fail randomly!
qa_chain = create_qa_chain()
answer = qa_chain.run("What is the capital of France?")
assert "Paris" in answer # Sometimes fails!
After (SteadyText - Reliable):
def test_qa_function():
# Always passes with deterministic output
answer = answer_question("What is the capital of France?")
assert answer == "The capital of France is Paris." # Exact match!
Migration Strategy¶
Phase 1: Replace Simple Chains¶
# Start with single-step operations
# Replace: LLMChain → steadytext.generate()
# Replace: embedding + vectorstore → PostgreSQL + pgvector
Phase 2: Simplify Complex Chains¶
# Convert multi-step chains to simple functions
# Remove unnecessary abstractions
# Use straightforward Python logic
Phase 3: Eliminate External Dependencies¶
Common Pitfalls & Solutions¶
1. Over-Engineering¶
# ❌ LangChain habit: Creating chains for everything
chain = LLMChain(llm=llm, prompt=PromptTemplate(...))
# ✅ SteadyText: Just call the function
result = steadytext.generate("Your prompt here")
2. Callback Complexity¶
# ❌ LangChain: Complex callback systems
callbacks = [StreamingStdOutCallbackHandler(), CustomCallback()]
# ✅ SteadyText: Simple iteration
for token in steadytext.generate_iter("Your prompt"):
print(token, end="", flush=True)
3. Configuration Overload¶
# ❌ LangChain: Tons of configuration
llm = ChatOpenAI(
model_name="gpt-3.5-turbo",
temperature=0,
max_tokens=100,
model_kwargs={"top_p": 0.9},
callbacks=callbacks,
cache=True
)
# ✅ SteadyText: Sensible defaults
result = steadytext.generate("prompt", max_tokens=100)
Real-World Migration Example¶
Here's a complete migration of a document QA system:
Before (LangChain - 150+ lines)¶
# Complex setup with multiple files, classes, and configurations
# Vector stores, embeddings, chains, callbacks, etc.
After (SteadyText - 20 lines)¶
-- Complete QA system in PostgreSQL
CREATE OR REPLACE FUNCTION qa_system(question TEXT)
RETURNS TEXT AS $$
DECLARE
context TEXT;
BEGIN
-- Find relevant content
SELECT string_agg(content, E'\n') INTO context
FROM documents
WHERE embedding <=> steadytext_embed(question)::vector < 0.3
ORDER BY embedding <=> steadytext_embed(question)::vector
LIMIT 3;
-- Generate answer
RETURN steadytext_generate(
format('Context: %s\n\nQuestion: %s\n\nAnswer:',
context, question)
);
END;
$$ LANGUAGE plpgsql;
-- Usage
SELECT qa_system('What is the main topic?');
Migration Checklist¶
- [ ] List all LangChain components in use
- [ ] Identify which can be replaced with simple functions
- [ ] Install SteadyText
- [ ] Migrate embeddings to PostgreSQL
- [ ] Replace chains with direct function calls
- [ ] Remove prompt templates (use f-strings)
- [ ] Simplify memory management
- [ ] Update tests for deterministic outputs
- [ ] Remove all API key management
- [ ] Delete unused dependencies
- [ ] Celebrate simpler code! 🎉
Next Steps¶
The Beauty of Simplicity
Most LangChain applications can be reduced to 10% of their original code size while gaining determinism, speed, and reliability. Less abstraction = fewer bugs!