Building an LLM Application with Langchain (2026 refactor)
Two years ago I refactored this code to fix a Chroma issue on Windows, add argument passing, and clean up the structure. Turns out two years is a long time in Langchain years. The code still runs, but it now throws deprecation warnings at you the whole way through, and there’s a newer, cleaner pattern for the actual question-answering part that’s worth switching to.
The code is being updated for three main reasons:
RetrievalQAis deprecated and will eventually be removed.- The completion-style model I was using (
gpt-3.5-turbo-instruct) has been superseded by chat models. - A bit of defensive code in
extract_datawas never actually doing anything useful.
You can find the updated code for this post on GitHub (coming soon!).
The RetrievalQA Problem
RetrievalQA.from_chain_type() was the easy button for question-answering when I wrote the last version of this post. It’s still the easy button, but Langchain has been deprecating it since version 0.1.17, and the old chains have since been shuffled off into a langchain_classic package for backward compatibility. That’s your sign it’s time to move on.
The replacement is create_retrieval_chain paired with create_stuff_documents_chain. It’s a couple more lines than RetrievalQA, but you get an explicit prompt template instead of a black box, which I actually prefer once you get past the extra typing.
Swapping the Model
While I was in there, I also swapped OpenAI(model_name="gpt-3.5-turbo-instruct", ...) for ChatOpenAI(). The old completions-style model still works, but chat models are where the ecosystem’s attention (and the model improvements) have been going, and the new chain helpers are built around chat models anyway.
Cleaning Up extract_data
The old extract_data function had an isinstance check for whether each loaded document was a dict or a string:
if isinstance(doc, dict) and 'text' in doc:
policy_text += doc['text']
elif isinstance(doc, str):
policy_text += doc
else:
policy_text += repr(doc)
I honestly don’t remember why I wrote it that way. PyPDFLoader.load() returns a list of Document objects with a .page_content attribute, full stop. So that got simplified down to one line.
Updated Version
Here is the updated script:
import os
import sys
from dotenv import load_dotenv
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import FAISS
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate
load_dotenv()
def main():
question = sys.argv[1]
pdf_name = sys.argv[2]
api_key = os.getenv("OPENAI_API_KEY")
text = extract_data(pdf_name)
docs = split_text(text)
docstorage = vectorize_and_store(docs, api_key)
response = answer_question(question, api_key, docstorage)
print(response['answer'])
# return response
def extract_data(pdf_name):
loader = PyPDFLoader(pdf_name)
data = loader.load()
policy_text = ""
for doc in data:
policy_text += doc.page_content
return policy_text
def split_text(text):
ct_splitter = CharacterTextSplitter(separator='.', chunk_size=1000, chunk_overlap=200)
docs = ct_splitter.split_text(text)
return docs
def vectorize_and_store(docs, api_key):
embedding_function = OpenAIEmbeddings(openai_api_key=api_key)
docstorage = FAISS.from_texts(docs, embedding_function)
return docstorage
def answer_question(question, api_key, docstorage):
llm = ChatOpenAI(model="gpt-4o-mini", openai_api_key=api_key)
system_prompt = (
"Use the given context to answer the question. "
"If you don't know the answer, say you don't know. "
"Use three sentences maximum and keep the answer concise.\n\n"
"Context: {context}"
)
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("human", "{input}"),
])
question_answer_chain = create_stuff_documents_chain(llm, prompt)
qa = create_retrieval_chain(docstorage.as_retriever(), question_answer_chain)
response = qa.invoke({"input": question})
return response
main()
Running the Script
Running it looks exactly the same as before — same two arguments, same idea:
$ python llm_faiss_vectorstore.py "Can I receive a blanket approval for outside work?" "pm-11.pdf"
No, blanket approvals for outside employment will not be granted. Approval for outside work must be obtained through normal administrative channels by the Chancellor or a designated campus administrative officer.
The only real difference under the hood: the response dictionary now comes back with an answer key instead of result, since that’s what create_retrieval_chain hands you. If you’re migrating your own code from the old version of this post, that’s the gotcha most likely to bite you.