Title: LangChain — LearnCoachAssist

Q: What is LangChain?
A: LangChain is an open-source Python framework for building applications powered by large language models (LLMs). It provides modular components for prompts, chains, agents, memory, and retrieval to compose complex LLM workflows.
pip install langchain langchain-openai

Q: What is LCEL (LangChain Expression Language)?
A: LCEL is a declarative syntax for composing LangChain components using the pipe operator (|). It creates RunnableSequence chains that support streaming, batching, and async out of the box.
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
chain = (
    ChatPromptTemplate.from_template("Explain {topic} simply.")
    | ChatOpenAI(model="gpt-4o")
    | StrOutputParser()
)
result = chain.invoke({"topic": "recursion"})

Q: How do you create a ChatPromptTemplate?
A: Use ChatPromptTemplate.from_messages() with a list of (role, template) tuples. Variables are wrapped in curly braces.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful {role}."),
    ("human", "{question}"),
])
messages = prompt.invoke({
    "role": "tutor",
    "question": "What is a linked list?"
})

Q: How do you invoke a ChatModel in LangChain?
A: Instantiate the model class and call .invoke() with a list of messages or a prompt value. The model returns an AIMessage.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke([
    HumanMessage(content="What is LangChain?")
])
print(response.content)

Q: How do you stream responses in LangChain?
A: Call .stream() instead of .invoke() on any Runnable. It returns an iterator of chunks you can process incrementally.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o")
for chunk in llm.stream("Tell me a joke"):
    print(chunk.content, end="", flush=True)

Q: What is a PromptTemplate in LangChain?
A: A PromptTemplate formats a single string prompt with variable substitution. Use it for completion-style models.
from langchain_core.prompts import PromptTemplate
template = PromptTemplate.from_template(
    "Summarize this text in {n} words:\n\n{text}"
)
prompt = template.invoke({"n": 50, "text": "LangChain is..."})

Q: What is a FewShotPromptTemplate?
A: A template that dynamically injects example input/output pairs into the prompt to guide the model via in-context learning.
from langchain_core.prompts import FewShotPromptTemplate, PromptTemplate
examples = [
    {"input": "happy", "output": "sad"},
    {"input": "tall", "output": "short"},
]
example_prompt = PromptTemplate.from_template(
    "Input: {input}\nOutput: {output}"
)
few_shot = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
    prefix="Give the antonym of the input.",
    suffix="Input: {input}\nOutput:",
    input_variables=["input"],
)
print(few_shot.invoke({"input": "big"}).text)

Q: What is MessagesPlaceholder?
A: MessagesPlaceholder inserts a dynamic list of messages (e.g., chat history) into a ChatPromptTemplate.
from langchain_core.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
)
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    MessagesPlaceholder("chat_history"),
    ("human", "{input}"),
])

Q: What is StrOutputParser?
A: StrOutputParser extracts the plain text string from an AIMessage. It's the most common output parser used at the end of LCEL chains.
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
chain = ChatOpenAI() | StrOutputParser()
text = chain.invoke("Say hello")  # returns a plain string

Q: What is JsonOutputParser?
A: JsonOutputParser parses the LLM's response into a Python dictionary. You can optionally provide a Pydantic model for validation.
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
class Joke(BaseModel):
    setup: str = Field(description="The setup")
    punchline: str = Field(description="The punchline")
parser = JsonOutputParser(pydantic_object=Joke)
prompt = prompt.partial(
    format_instructions=parser.get_format_instructions()
)

Q: What is PydanticOutputParser?
A: PydanticOutputParser parses LLM output directly into a Pydantic model instance with validation and error messages.
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel
class Person(BaseModel):
    name: str
    age: int
parser = PydanticOutputParser(pydantic_object=Person)
chain = prompt | llm | parser
person = chain.invoke({"query": "Tell me about Alice, age 30"})
print(person.name, person.age)

Q: What is RunnableSequence?
A: A RunnableSequence chains runnables so the output of one becomes the input of the next. Created automatically when you use the | pipe operator in LCEL.
from langchain_core.runnables import RunnableSequence
# These are equivalent:
chain = prompt | llm | parser
chain = RunnableSequence(first=prompt, middle=[llm], last=parser)
result = chain.invoke({"topic": "AI"})

Q: What is RunnableParallel?
A: RunnableParallel runs multiple runnables concurrently and returns a dict of results. Useful for branching logic.
from langchain_core.runnables import RunnableParallel
chain = RunnableParallel(
    summary=summary_chain,
    translation=translate_chain,
    keywords=keyword_chain,
)
results = chain.invoke({"text": "LangChain is..."})
# results = {"summary": ..., "translation": ..., "keywords": ...}

Q: What is RunnablePassthrough?
A: RunnablePassthrough passes its input through unchanged. Commonly used in RAG chains to forward the original question alongside retrieved context.
from langchain_core.runnables import RunnablePassthrough
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
answer = chain.invoke("What is LangChain?")

Q: What is RunnableLambda?
A: RunnableLambda wraps any Python function as a LangChain Runnable so it can be composed in LCEL chains.
from langchain_core.runnables import RunnableLambda
def word_count(text: str) -> int:
    return len(text.split())
chain = llm | StrOutputParser() | RunnableLambda(word_count)
count = chain.invoke("Write a short poem")

Q: How do you add fallbacks to a chain?
A: Use .with_fallbacks() to specify alternative runnables if the primary one fails (e.g., a cheaper model as backup).
from langchain_openai import ChatOpenAI
primary = ChatOpenAI(model="gpt-4o")
fallback = ChatOpenAI(model="gpt-4o-mini")
llm_with_fallback = primary.with_fallbacks([fallback])
response = llm_with_fallback.invoke("Hello")

Q: How do you use RunnableBranch for conditional logic?
A: RunnableBranch routes input to different chains based on conditions. Pass tuples of (condition, runnable) plus a default.
from langchain_core.runnables import RunnableBranch
branch = RunnableBranch(
    (lambda x: "code" in x["topic"], code_chain),
    (lambda x: "math" in x["topic"], math_chain),
    general_chain,  # default
)
result = branch.invoke({"topic": "code review"})

Q: What is ConversationBufferMemory?
A: ConversationBufferMemory stores the full conversation history as a list of messages. Simple but uses more tokens as the conversation grows.
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(return_messages=True)
memory.save_context(
    {"input": "Hi, I'm Alice"},
    {"output": "Hello Alice! How can I help?"}
)
print(memory.load_memory_variables({}))

Q: What is ConversationSummaryMemory?
A: ConversationSummaryMemory uses an LLM to summarize the conversation history, keeping token usage low for long conversations.
from langchain.memory import ConversationSummaryMemory
from langchain_openai import ChatOpenAI
memory = ConversationSummaryMemory(
    llm=ChatOpenAI(model="gpt-4o-mini"),
    return_messages=True,
)
memory.save_context(
    {"input": "Tell me about Mars"},
    {"output": "Mars is the fourth planet..."}
)

Q: How do you use RunnableWithMessageHistory?
A: RunnableWithMessageHistory wraps a chain to automatically manage chat history per session using a history factory function.
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory
store = {}
def get_history(session_id: str):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]
with_history = RunnableWithMessageHistory(
    chain,
    get_history,
    input_messages_key="input",
    history_messages_key="chat_history",
)
result = with_history.invoke(
    {"input": "Hi"},
    config={"configurable": {"session_id": "abc123"}},
)

Q: How do you create a custom tool with the @tool decorator?
A: The @tool decorator turns any Python function into a LangChain tool. The docstring becomes the tool description the agent uses to decide when to call it.
from langchain_core.tools import tool
@tool
def multiply(a: int, b: int) -> int:
    """Multiply two integers together."""
    return a * b
print(multiply.name)         # "multiply"
print(multiply.description)  # "Multiply two integers..."
result = multiply.invoke({"a": 3, "b": 4})  # 12

Q: How do you create a tool with StructuredTool?
A: StructuredTool lets you define tools with explicit Pydantic schemas for complex input validation.
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
class SearchInput(BaseModel):
    query: str = Field(description="Search query")
    max_results: int = Field(default=5)
def search(query: str, max_results: int = 5) -> str:
    return f"Results for '{query}' (top {max_results})"
search_tool = StructuredTool.from_function(
    func=search,
    name="web_search",
    description="Search the web",
    args_schema=SearchInput,
)

Q: How do you create a ReAct agent?
A: Use create_react_agent() to build an agent that reasons step-by-step and calls tools. Pair it with a tool-calling LLM.
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
llm = ChatOpenAI(model="gpt-4o")
tools = [multiply, web_search]
agent = create_react_agent(llm, tools)
result = agent.invoke(
    {"messages": [("human", "What is 25 * 48?")]}
)
print(result["messages"][-1].content)

Q: What is a Document in LangChain?
A: A Document is a data class with page_content (text string) and metadata (dict). All loaders, splitters, and retrievers work with Documents.
from langchain_core.documents import Document
doc = Document(
    page_content="LangChain is a framework for LLMs.",
    metadata={"source": "docs", "page": 1}
)
print(doc.page_content)
print(doc.metadata)

Q: How do you load documents with a DocumentLoader?
A: LangChain provides loaders for many file types. Each returns a list of Document objects.
from langchain_community.document_loaders import (
    TextLoader,
    PyPDFLoader,
    WebBaseLoader,
)
# Load a text file
docs = TextLoader("notes.txt").load()
# Load a PDF
docs = PyPDFLoader("paper.pdf").load()
# Load a web page
docs = WebBaseLoader("https://example.com").load()

Q: How do you split documents with a TextSplitter?
A: Text splitters break large documents into smaller chunks for embedding. RecursiveCharacterTextSplitter is the most commonly used.
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,
    chunk_overlap=200,
    separators=["\n\n", "\n", ". ", " "],
)
chunks = splitter.split_documents(docs)
print(f"{len(chunks)} chunks created")

Q: How do you create embeddings in LangChain?
A: Embeddings convert text into numerical vectors for similarity search. Instantiate an embeddings model and call .embed_documents() or .embed_query().
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
# Embed multiple documents
vectors = embeddings.embed_documents(["Hello", "World"])
# Embed a single query
query_vec = embeddings.embed_query("What is AI?")
print(len(query_vec))  # e.g., 1536

Q: How do you create and query a vector store?
A: Vector stores index document embeddings for similarity search. FAISS is a popular local option.
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
# Create from documents
vectorstore = FAISS.from_documents(
    documents=chunks,
    embedding=OpenAIEmbeddings(),
)
# Similarity search
results = vectorstore.similarity_search(
    "How does LangChain work?", k=3
)
for doc in results:
    print(doc.page_content[:100])

Q: How do you create a retriever from a vector store?
A: Call .as_retriever() on any vector store to get a Retriever that works in LCEL chains. You can configure search type and parameters.
retriever = vectorstore.as_retriever(
    search_type="similarity",
    search_kwargs={"k": 4},
)
# Use in a chain
docs = retriever.invoke("What is LangChain?")
for doc in docs:
    print(doc.page_content[:80])

Q: How do you build a RAG (Retrieval-Augmented Generation) chain?
A: Combine a retriever, prompt, and LLM using LCEL. RunnablePassthrough forwards the question alongside retrieved context.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
prompt = ChatPromptTemplate.from_template(
    "Answer based on context:\n{context}\n\nQuestion: {question}"
)
def format_docs(docs):
    return "\n\n".join(d.page_content for d in docs)
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)
answer = rag_chain.invoke("What is LangChain?")

Q: What is ContextualCompressionRetriever?
A: A retriever wrapper that compresses or filters retrieved documents using an LLM or embeddings to keep only the most relevant content.
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainExtractor
compressor = LLMChainExtractor.from_llm(llm)
compression_retriever = ContextualCompressionRetriever(
    base_compressor=compressor,
    base_retriever=retriever,
)
docs = compression_retriever.invoke("What is LCEL?")

Q: How do you use callbacks in LangChain?
A: Callbacks let you hook into chain execution for logging, tracing, or custom side effects. Pass them via config or set them globally.
from langchain_core.callbacks import BaseCallbackHandler
class LogHandler(BaseCallbackHandler):
    def on_llm_start(self, serialized, prompts, **kwargs):
        print(f"LLM started with {len(prompts)} prompts")
    def on_llm_end(self, response, **kwargs):
        print(f"LLM finished")
chain.invoke(
    {"topic": "AI"},
    config={"callbacks": [LogHandler()]}
)

Q: How do you use LangSmith for tracing?
A: Set environment variables to enable LangSmith tracing. All LangChain runs are automatically logged for debugging and evaluation.
import os
os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_API_KEY"] = "your-key"
os.environ["LANGSMITH_PROJECT"] = "my-project"
# All chain invocations are now traced
result = chain.invoke({"topic": "LangSmith"})
# View traces at smith.langchain.com

Q: What is LangGraph?
A: LangGraph is a library by LangChain for building stateful, multi-step agent workflows as directed graphs. Nodes are functions, edges define transitions.
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class State(TypedDict):
    messages: list
    next_step: str
graph = StateGraph(State)
graph.add_node("classify", classify_fn)
graph.add_node("respond", respond_fn)
graph.add_edge(START, "classify")
graph.add_edge("classify", "respond")
graph.add_edge("respond", END)
app = graph.compile()

Q: How do you batch invoke a chain?
A: Use .batch() to process multiple inputs concurrently. It accepts a list of inputs and returns a list of outputs.
chain = prompt | llm | StrOutputParser()
results = chain.batch([
    {"topic": "Python"},
    {"topic": "JavaScript"},
    {"topic": "Rust"},
], config={"max_concurrency": 3})
for r in results:
    print(r[:100])

Q: How do you use async invoke in LangChain?
A: Call .ainvoke(), .astream(), or .abatch() for async execution. Works with Python's asyncio.
import asyncio
async def main():
    result = await chain.ainvoke({"topic": "async"})
    print(result)
    async for chunk in chain.astream({"topic": "streaming"}):
        print(chunk, end="")
asyncio.run(main())

Q: How do you bind tools to a ChatModel?
A: Use .bind_tools() to attach tool definitions to a model so it can generate tool-calling messages.
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"Sunny in {city}"
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools([get_weather])
response = llm_with_tools.invoke("Weather in Paris?")
print(response.tool_calls)

Q: How do you use output_parsers with format_instructions?
A: Many parsers provide get_format_instructions() which returns a string you inject into the prompt to guide the LLM's output format.
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
parser = JsonOutputParser()
prompt = PromptTemplate(
    template="List 3 facts about {topic}.\n{format_instructions}",
    input_variables=["topic"],
    partial_variables={
        "format_instructions": parser.get_format_instructions()
    },
)
chain = prompt | llm | parser
facts = chain.invoke({"topic": "Python"})

Q: How do you split code files with a language-aware splitter?
A: Use Language enum with RecursiveCharacterTextSplitter.from_language() to split code respecting function and class boundaries.
from langchain_text_splitters import (
    RecursiveCharacterTextSplitter,
    Language,
)
python_splitter = RecursiveCharacterTextSplitter.from_language(
    language=Language.PYTHON,
    chunk_size=1000,
    chunk_overlap=100,
)
chunks = python_splitter.split_documents(code_docs)

Q: How do you use Chroma as a vector store?
A: Chroma is an open-source embedding database that supports persistent local storage.
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
# Create and persist
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=OpenAIEmbeddings(),
    persist_directory="./chroma_db",
)
# Load existing
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=OpenAIEmbeddings(),
)
results = vectorstore.similarity_search("LangChain", k=3)

Q: How do you use a MultiQueryRetriever?
A: MultiQueryRetriever generates multiple query variations using an LLM, retrieves documents for each, and merges the results for better recall.
from langchain.retrievers.multi_query import MultiQueryRetriever
retriever = MultiQueryRetriever.from_llm(
    retriever=vectorstore.as_retriever(),
    llm=ChatOpenAI(model="gpt-4o-mini"),
)
docs = retriever.invoke("How does memory work in LangChain?")

Q: How do you configure model parameters in LangChain?
A: Pass parameters like temperature, max_tokens, and model to the constructor. Use .bind() for per-call overrides.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0.2,
    max_tokens=500,
    request_timeout=30,
)
# Override per call
creative_llm = llm.bind(temperature=0.9)
result = creative_llm.invoke("Write a poem")

Q: How do you use with_structured_output?
A: .with_structured_output() binds a Pydantic model or JSON schema to the LLM so it returns validated structured data directly.
from langchain_openai import ChatOpenAI
from pydantic import BaseModel
class MovieReview(BaseModel):
    title: str
    rating: float
    summary: str
llm = ChatOpenAI(model="gpt-4o")
structured_llm = llm.with_structured_output(MovieReview)
review = structured_llm.invoke("Review The Matrix")
print(review.title, review.rating)

Q: How do you create a chain with retry logic?
A: Use .with_retry() to add automatic retries with exponential backoff to any Runnable in case of transient errors.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o").with_retry(
    stop_after_attempt=3,
    wait_exponential_jitter=True,
)
result = llm.invoke("Hello")

Q: How do you load environment variables for LangChain API keys?
A: Use dotenv or set environment variables directly. LangChain reads OPENAI_API_KEY and other keys automatically.
from dotenv import load_dotenv
import os
load_dotenv()  # reads .env file
# Or set directly
os.environ["OPENAI_API_KEY"] = "sk-..."
# LangChain auto-detects the key
from langchain_openai import ChatOpenAI
llm = ChatOpenAI()  # uses OPENAI_API_KEY

Q: How do you use ChatMessageHistory for manual history management?
A: ChatMessageHistory is a simple in-memory message store for managing conversation turns manually.
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage
history = ChatMessageHistory()
history.add_user_message("Hi!")
history.add_ai_message("Hello! How can I help?")
history.add_message(HumanMessage(content="What is AI?"))
print(history.messages)  # list of BaseMessage objects

Q: How do you install LangChain and its integrations?
A: Use pip install for the core packages. The base langchain package is now split into langchain-core, langchain (the orchestration layer), and partner packages like langchain-openai, langchain-anthropic, langchain-community.
pip install langchain langchain-core langchain-openai langchain-community

Q: What is langchain-core?
A: langchain-core contains the base abstractions (Runnable, BaseMessage, BaseChatModel, BaseRetriever, Document) used by the rest of the framework. It has no third-party LLM/provider dependencies and is the only package you need for building custom integrations.

Q: What is langchain-community?
A: langchain-community is a community-maintained package containing hundreds of loaders, vector stores, retrievers, and integrations contributed by the community. Quality varies, and stable production code is increasingly being moved to dedicated partner packages.

Q: What is a Runnable in LangChain?
A: A Runnable is the base interface for any component that can be invoked, streamed, batched, and composed in LCEL. All chat models, prompts, retrievers, output parsers, and even Python functions (via RunnableLambda) implement it.
class Runnable(Generic[Input, Output]):
    def invoke(self, input, config=None) -> Output: ...
    async def ainvoke(self, input, config=None) -> Output: ...
    def stream(self, input, config=None) -> Iterator[Output]: ...
    def batch(self, inputs, config=None) -> List[Output]: ...

Q: What methods does the Runnable interface guarantee?
A: Every Runnable supports invoke, ainvoke, stream, astream, batch, abatch, astream_log, and transform. This uniform interface is what makes LCEL composition work across any component type.

Q: What is BaseChatModel?
A: BaseChatModel is the abstract base class for chat-style models (GPT-4o, Claude, Gemini). Subclasses implement _generate and _stream; everything else (tool calling, structured output, batching) is provided by the framework.

Q: What is the difference between ChatModel and LLM?
A: A ChatModel takes a list of BaseMessage objects and returns an AIMessage, supporting multi-turn conversations and tool calls. The legacy LLM class takes a string and returns a string; it is largely deprecated in favor of chat models.

Q: What are the message types in langchain-core?
A: SystemMessage, HumanMessage, AIMessage, ToolMessage, FunctionMessage, and the abstract BaseMessage. Each has a content field and an optional additional_kwargs / response_metadata dict.

Q: What is AIMessage.tool_calls?
A: tool_calls is a list of dicts on an AIMessage describing the tools the model wants to invoke. Each entry has id, name, and args (parsed arguments) plus type and optional artifact.

Q: What is ToolMessage used for?
A: ToolMessage carries the result of a tool execution back to the model in the next conversation turn. It must include the tool_call_id matching the AIMessage.tool_calls[i].id it answers.

Q: How do you invoke a tool that an LLM requested?
A: Look up the tool by name, call it with the arguments from tool_calls, and append a ToolMessage with the matching tool_call_id.
tools_by_name = {t.name: t for t in tools}
for tc in response.tool_calls:
    result = tools_by_name[tc["name"]].invoke(tc["args"])
    messages.append(ToolMessage(
        content=result, tool_call_id=tc["id"]
    ))

Q: What does .bind() do on a Runnable?
A: .bind() returns a new Runnable with extra keyword arguments frozen onto every call. Useful for binding stop sequences, tools, or response_format without rewriting the chain.
creative = llm.bind(temperature=0.9, stop=["\n\n"])
short = llm.bind(max_tokens=50)

Q: What does .assign() do in an LCEL chain?
A: .assign() on a RunnableParallel adds a new key to the output dict while preserving the original input. It enables map-step-style data flow inside a chain.
chain = (
    RunnablePassthrough.assign(
        context=lambda x: retriever.invoke(x["question"])
    )
    | prompt | llm | parser
)

Q: How do you use .pick() to select a single branch output?
A: .pick("key") returns a runnable that pulls one key out of a dict (or one index out of a tuple). It short-circuits upstream computation by skipping unselected branches.
full = RunnableParallel(
    summary=summary_chain,
    keywords=keyword_chain,
    sentiment=sentiment_chain,
)
just_summary = full.pick("summary")

Q: What is RunnableConfig?
A: RunnableConfig is a TypedDict passed via config= that carries callbacks, tags, metadata, run_name, configurable, and recursion_limit across the entire chain during one invocation.

Q: What is the configurable field in RunnableConfig?
A: The configurable dict lets you pass runtime values that configurable_alternatives and configurable_fields on a chain can read to swap models, prompts, or other parameters at invoke time without rebuilding the chain.

Q: How do you make a field runtime-configurable?
A: Wrap a value with configurable_fields so it can be overridden per call from config["configurable"].
llm = ChatOpenAI(
    temperature=ConfigurableField(
        id="llm_temp", name="LLM temperature"
    )
)
llm.invoke("hi", config={"configurable": {"llm_temp": 0.9}})

Q: What is the difference between .with_fallbacks() and .with_retry()?
A: .with_retry() retries the same runnable on transient errors (rate limits, timeouts). .with_fallbacks() runs an alternative runnable when the primary raises a non-retried exception. You can compose both.

Q: How do you get a Runnable's schema and input/output types?
A: Call runnable.get_input_schema() and get_output_schema() (Pydantic models). get_graph() returns a drawable graph; get_prompts() lists prompts nested in the chain — useful for LangSmith inspection.

Q: What is the RunnableLambda.from_runnable decorator?
A: @RunnableLambda.from_runnable is used inside a RunnableGenerator / async generator-style custom runnable when you need to implement transform / astream directly while still benefiting from config propagation.

Q: What is a token in an LCEL chain?
A: In LCEL, a "token" is a chunk yielded by a streaming runnable. The pipe operator ensures tokens flow through the chain as they are produced, so the final output starts streaming immediately instead of waiting for the whole LLM response.

Q: How do you stream events from a chain?
A: Use chain.astream_events(input, version="v2") to receive typed events (on_chat_model_start, on_tool_start, on_chain_end, etc.) as the chain executes. This is the most granular streaming API.

Q: What is LangChain Hub?
A: LangChain Hub is a public registry of reusable prompts, agents, and chains that you can pull by name. The langchainhub package exposes hub.pull("owner/repo-name") which returns a ChatPromptTemplate or Runnable directly.

Q: What is a PromptValue?
A: A PromptValue is the intermediate output of a prompt template — it has .to_string() and .to_messages() methods so the same prompt can be fed to either a string LLM or a chat model without rewriting the template.

Q: How do you compose two parallel branches into a single string?
A: Use RunnableParallel for the branches, then merge with a RunnableLambda or a prompt that consumes the dict. A common pattern is to .assign() one branch's output and format it inside the final prompt.

Q: What is the difference between invoke and stream for an LCEL chain?
A: invoke returns the final output after the entire chain finishes. stream returns an iterator that yields intermediate chunks (often just the final parser's tokens). For a model | parser chain, the chunks are the model's text deltas.

Q: What is ConversationBufferWindowMemory?
A: ConversationBufferWindowMemory keeps only the last k turns of conversation, preventing unbounded token growth. It is the standard middle-ground between raw buffer and summary memory.
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(
    k=5, return_messages=True
)

Q: What is ConversationSummaryBufferMemory?
A: ConversationSummaryBufferMemory keeps recent messages in full and summarizes older ones once a token limit is reached. It balances fidelity (recent context) with token efficiency (summarized history).

Q: What is ConversationEntityMemory?
A: ConversationEntityMemory extracts named entities mentioned in the conversation and stores facts about each, then injects relevant entity info into future prompts. Useful for assistants that need to remember user-specific details.

Q: What is VectorStoreRetrieverMemory?
A: VectorStoreRetrieverMemory stores each conversation turn in a vector database and retrieves the k most semantically similar past turns at query time. It scales to very long histories without saturating the context window.

Q: What is the modern way to add memory in LangChain?
A: In current LangChain, the recommended pattern is to manage messages yourself (or via ChatMessageHistory) and inject them with MessagesPlaceholder or RunnableWithMessageHistory. The old Memory classes in langchain.memory are legacy and slated for deprecation.

Q: How do you persist chat history across sessions?
A: Swap the in-memory ChatMessageHistory for a persistent implementation: RedisChatMessageHistory, PostgresChatMessageHistory, SQLChatMessageHistory, or a custom subclass. Pass it to the same get_session_history factory.

Q: What is the difference between store and session history?
A: store is a cross-session BaseStore (key/value) used for long-term facts (e.g., user preferences). Session history is the per-thread message list. LangChain exposes both through get_store and get_session_history callbacks in RunnableWithMessageHistory.

Q: How do you create a custom Retriever in LangChain?
A: Subclass BaseRetriever and implement _get_relevant_documents (and optionally _aget_relevant_documents for async). Set the class attribute model_config = ConfigDict(arbitrary_types_allowed=True) and the lc_namespace for serialization.

Q: How do you create a custom VectorStore?
A: Subclass VectorStore and implement from_texts, add_texts, similarity_search, and optionally delete, from_documents, and the async variants. The base class also requires __init__ accepting an embedding function.

Q: What is FAISS in LangChain?
A: FAISS (Facebook AI Similarity Search) is an in-memory vector index. LangChain wraps it via FAISS.from_documents and FAISS.from_texts. It is fast and dependency-free but does not persist between processes unless you call save_local / load_local.

Q: How do you persist a FAISS index to disk?
A: Call vectorstore.save_local("path") and reload with FAISS.load_local("path", embeddings, allow_dangerous_deserialization=True). The flag exists because pickle loading is unsafe for untrusted files.

Q: What is MMR (Maximal Marginal Relevance) search?
A: MMR selects documents that are both relevant to the query and diverse relative to already-selected documents. It reduces redundancy in long-context retrieval.
retriever = vectorstore.as_retriever(
    search_type="mmr",
    search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.5},
)

Q: What is similarity_score_threshold retrieval?
A: A search type that returns only documents with a similarity score above a threshold (normalized 0–1 for cosine). Useful for filtering out noise from low-quality chunks.
retriever = vectorstore.as_retriever(
    search_type="similarity_score_threshold",
    search_kwargs={"score_threshold": 0.8},
)

Q: What is SelfQueryRetriever?
A: SelfQueryRetriever uses an LLM to translate a natural-language query into a structured query (semantic + metadata filters). The vector store must support metadata filtering.
from langchain.retrievers.self_query.base import SelfQueryRetriever
retriever = SelfQueryRetriever.from_llm(
    llm=llm,
    vectorstore=vectorstore,
    document_content_description="AI/ML papers",
    metadata_field_info=[...],
)

Q: What is ParentDocumentRetriever?
A: ParentDocumentRetriever indexes small chunks (for precise retrieval) but returns the larger parent document they came from, so the LLM sees more context. It stores parent-child IDs in a Docstore.

Q: What is EnsembleRetriever?
A: EnsembleRetriever combines results from multiple retrievers (e.g., BM25 + vector) using Reciprocal Rank Fusion. You assign weights to each retriever to control the blend.
from langchain.retrievers import EnsembleRetriever
retriever = EnsembleRetriever(
    retrievers=[bm25, vector], weights=[0.4, 0.6]
)

Q: What is BM25Retriever?
A: BM25Retriever is a keyword-based (lexical) retriever from langchain_community.retrievers.bm25. It complements semantic search for exact term matches (function names, error codes, proper nouns).
from langchain_community.retrievers import BM25Retriever
retriever = BM25Retriever.from_documents(docs, k=5)

Q: What is the base Document class and its fields?
A: Document in langchain_core.documents has two fields: page_content: str and metadata: dict. Both metadata and page_content are immutable in the sense that updates should create a new Document (the underlying objects are not deep-copied by default).

Q: What is RecursiveCharacterTextSplitter and how do its separators work?
A: RecursiveCharacterTextSplitter tries the separators in order: ["\n\n", "\n", " ", ""]. For each chunk, it picks the first separator that yields pieces under chunk_size. This keeps semantically related text together (paragraphs > sentences > words).

Q: What is the difference between chunk_size and chunk_overlap?
A: chunk_size is the maximum length (in characters or tokens, depending on length_function) of each chunk. chunk_overlap is the number of characters from the end of one chunk repeated at the start of the next — it prevents context loss at chunk boundaries.

Q: What is CharacterTextSplitter?
A: A simpler splitter that splits on a single separator string (default "\n\n"). Use it when you know the document structure is uniform (e.g., one paragraph per record).
from langchain_text_splitters import CharacterTextSplitter
splitter = CharacterTextSplitter(
    separator="\n", chunk_size=500, chunk_overlap=50
)

Q: What is TokenTextSplitter?
A: TokenTextSplitter splits based on token count (using tiktoken) instead of characters. Useful when you need chunks that respect the model's tokenizer limits precisely.
from langchain_text_splitters import TokenTextSplitter
splitter = TokenTextSplitter(
    encoding_name="cl100k_base",
    chunk_size=512, chunk_overlap=50,
)

Q: What is the HTML splitter in LangChain?
A: HTMLHeaderTextSplitter splits HTML by header tags (h1–h6) and uses the headers as metadata for each chunk. HTMLSectionSplitter splits by <section> elements. Useful for documentation and web pages.

Q: What is MarkdownTextSplitter?
A: MarkdownTextSplitter splits Markdown by its syntax structure — headers, code blocks, lists — preserving the formatting context as metadata. ExperimentalMarkdownSyntaxTextSplitter adds richer metadata (code language, header paths).

Q: What is SemanticChunker?
A: SemanticChunker uses embeddings to detect natural breakpoints: it splits where the cosine distance between consecutive sentences exceeds a threshold. Chunks align with semantic topic shifts rather than fixed character counts.

Q: How do you create a custom OutputFixingParser?
A: Wrap any base parser with OutputFixingParser.from_llm(parser, llm). If the LLM output fails to parse, the wrapped parser uses the LLM to fix the error and re-parse, avoiding total chain failure.
from langchain.output_parsers import OutputFixingParser
parser = OutputFixingParser.from_llm(
    PydanticOutputParser(pydantic_object=Person), llm
)

Q: What is RetryWithErrorOutputParser?
A: Like OutputFixingParser, but it re-prompts the LLM with both the original output and the parser's error message, giving the model explicit feedback on what went wrong. Better for structurally complex schemas.

Q: What is CommaSeparatedListOutputParser?
A: Parses a comma-separated string into a Python List[str], trimming whitespace. Useful for "list 5 items" prompts.
from langchain_core.output_parsers import CommaSeparatedListOutputParser
parser = CommaSeparatedListOutputParser()

Q: What is DatetimeOutputParser?
A: Parses LLM output into a Python datetime using a Pydantic-style format string passed via format_instructions.
from langchain.output_parsers import DatetimeOutputParser
parser = DatetimeOutputParser(format="%Y-%m-%d")

Q: What is EnumOutputParser?
A: Parses LLM output into one of a fixed set of Enum values, validating that the model picked an allowed option. Useful for classification / routing tasks.

Q: What is XMLOutputParser?
A: Parses LLM output into a Python dict by traversing an XML schema. Useful when models are better at emitting XML than JSON (e.g., Anthropic, older models).

Q: What is StructuredOutputParser?
A: A parser for a list of named string response schemas. Returns a dict with the keys you declared. Less strict than Pydantic — no type coercion.

Q: What is the OpenAI Functions / Tools approach to structured output?
A: For OpenAI models, structured output is achieved natively by passing a JSON schema (or Pydantic model) to the tools / response_format parameter. with_structured_output is the unified LangChain wrapper for this.

Q: How do you use tool_choice to force a specific tool call?
A: Pass tool_choice="any" to force the model to call a tool, or tool_choice={"type": "tool", "name": "my_tool"} to force a specific one. Useful for routing decisions.
llm.bind_tools([a, b], tool_choice="a").invoke(query)

Q: What is .with_retry() and what arguments does it accept?
A: .with_retry() accepts stop_after_attempt, wait_exponential, wait_exponential_jitter, retry_if_exception_type, and a plain retry_on predicate. Backoff and jitter settings prevent thundering-herd rate limit failures.

Q: How do you cap a chain's LLM cost?
A: Combine .with_retry() for transient errors with a callback handler that records token usage. Then enforce a daily or per-user token budget by short-circuiting the chain when the budget is exceeded (often via a guard runnable or a router).

Q: What is a CallbackHandler event lifecycle?
A: For an LLM run the lifecycle is: on_llm_start → on_chat_model_start → on_llm_new_token (per chunk) → on_llm_end (or on_llm_error). For a chain: on_chain_start → child events → on_chain_end. For tools: on_tool_start → on_tool_end.

Q: What is the difference between on_chain_start and on_llm_start?
A: on_chain_start fires for any Runnable composition (your custom chain), while on_llm_start fires specifically for an LLM call with the prompts. Nested runnables emit nested start/end pairs you can correlate by run_id and parent_run_id.

Q: What is LangSmith and what does it cost?
A: LangSmith is a hosted observability + evaluation platform from the LangChain team. The free tier supports limited traces/month; paid plans add longer retention, datasets, evaluators, and collaboration features. Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY to enable.

Q: What is a Run Tree in LangSmith?
A: A Run Tree is the nested hierarchy of Run objects produced by a single invoke call. Each LLM call, retriever call, and tool call is a node; edges represent the data flow. You can replay any run by re-invoking the recorded inputs.

Q: How do you create a LangSmith dataset and run an evaluation?
A: Use Client.create_dataset to define examples (input + expected output), then Client.evaluate with an evaluator (e.g., label_score, cot_qa, or custom) to score your chain. Runs are logged under a project name for comparison.

Q: What is LangServe?
A: LangServe is a library that deploys any Runnable as a REST API using FastAPI. It exposes /invoke, /batch, /stream, /stream_log, and /playground endpoints automatically and generates a JSON schema for inputs/outputs.

Q: How do you serve a chain with LangServe?
A: Define your runnable, then in a FastAPI app use add_routes(app, runnable, path="/mychain"). Provide a RunnableConfig factory with configurable keys (model, prompts) for runtime overrides.
from langserve import add_routes
add_routes(app, chain, path="/rag")

Q: What is LangChain Templates?
A: A deprecated gallery of pre-built project templates (now archived). The current recommendation is the langgraph new CLI (for LangGraph projects) or the create-langchain-integration scaffold (for partner packages).

Q: What is LangGraph's StateGraph?
A: StateGraph is the core LangGraph primitive: a directed graph where nodes are functions/Runnables that read and update a shared typed state, and edges define the flow including conditional branching.
from langgraph.graph import StateGraph, START, END
builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_edge(START, "agent")
builder.add_conditional_edges("agent", should_continue)
builder.add_edge("agent", END)
app = builder.compile()

Q: What is a conditional edge in LangGraph?
A: A conditional edge routes to different nodes based on a function that inspects the current state. The routing function returns a node name (or list of names for parallel fan-out).
def should_continue(state):
    return "tools" if state["messages"][-1].tool_calls else END
builder.add_conditional_edges("agent", should_continue)

Q: What is a Checkpoint in LangGraph?
A: A Checkpoint is a snapshot of the graph's state at a point in time. The MemorySaver (in-memory) and SqliteSaver / PostgresSaver (persistent) checkpointers let you pause, resume, rewind, and branch conversations.

Q: What is a Thread in LangGraph?
A: A thread_id is the key passed to config={"configurable": {"thread_id": ...}} that scopes a conversation. Each thread has its own independent state, message history, and checkpoint trail — the equivalent of a chat session.

Q: What is human-in-the-loop in LangGraph?
A: You can interrupt the graph before a node runs (interrupt_before=["tool"]) or after (interrupt_after), inspect the state, edit it, and resume. This is the standard pattern for approving sensitive tool calls before execution.

Q: How do you stream updates from a LangGraph graph?
A: Use app.stream(input, config) for a synchronous iterator of state deltas, or app.astream_events(input, version="v2") for token-level streaming. Each yielded dict has a node name as its key.

Q: What is the ReAct prompt structure?
A: ReAct (Reason + Act) prompts alternate Thought (chain-of-thought reasoning), Action (which tool to call and with what args), Observation (tool result), and repeat until the model emits Final Answer. Modern function-calling agents replace the text action with structured tool_calls.

Q: What is the OpenAI Functions Agent?
A: An agent type that uses OpenAI's native function-calling API to decide tool invocations instead of parsing a ReAct text trace. It is more reliable than ReAct on OpenAI models and is the default for create_openai_functions_agent.

Q: What is a Tool Calling Agent?
A: A general name for an agent that uses native tool/function calling — supports any chat model with tool-calling support (OpenAI, Anthropic, Gemini, Mistral, etc.). In LangGraph, create_react_agent is the canonical tool-calling agent factory.

Q: What is the difference between an agent and a chain?
A: A chain has a deterministic, predefined sequence of steps. An agent uses an LLM at runtime to decide which tools to call, in what order, and how many times. Agents are non-deterministic and harder to test; chains are easier to debug but less flexible.

Q: What is the AgentExecutor?
A: Legacy class that runs an agent + tools in a loop, handling tool execution, error handling, and early stopping. In modern LangChain/LangGraph, you write this loop explicitly in a graph — AgentExecutor is being deprecated in favor of LangGraph.

Q: What is max_iterations in AgentExecutor?
A: A safety cap on the number of tool-call → observation cycles the agent can perform before the executor gives up and returns. Prevents runaway costs when the model loops.
executor = AgentExecutor(
    agent=agent, tools=tools, max_iterations=5
)

Q: What is early_stopping_method in AgentExecutor?
A: How the executor returns when max_iterations is hit: "force" returns the partial answer, "generate" asks the LLM one more time to summarize. Use "generate" for better graceful degradation.

Q: What is handle_parsing_errors in AgentExecutor?
A: When the LLM produces malformed tool calls, the executor can either raise (False), silently retry (True), or pass a custom error handler that crafts a corrective message.

Q: What is a tool's return_direct attribute?
A: If a tool's return_direct=True, the agent returns the tool's output to the user immediately instead of feeding it back to the LLM for summarization. Use for deterministic lookups (e.g., get_current_time).

Q: What is BaseTool and how does it differ from @tool?
A: BaseTool is the abstract class underlying every tool — _run, _arun, args_schema, name, description. The @tool decorator is a shortcut for the common case of wrapping a function; subclassing gives you full control (validation, async-only, dynamic schema).

Q: What is a ToolException?
A: Raising ToolException inside a tool makes the agent retry the call with the exception's message as feedback, rather than failing the whole chain. Use for recoverable errors like transient API failures.

Q: What is handle_tool_error on BaseTool?
A: When set on a tool instance, the agent catches the exception and passes the configured message back to the model. Equivalent to raising ToolException but configurable per-tool.
@tool(handle_tool_error=True)
def flaky(): ...

Q: How do you create a tool that returns artifacts?
A: Override _run to return a tuple (content, artifact). The content goes to the LLM; the artifact stays in the run state and can be inspected in LangSmith. Useful for tools that fetch raw data the LLM shouldn't see.

Q: What is the Embeddings interface in LangChain?
A: All embedding models implement embed_documents(texts: List[str]) -> List[List[float]] and embed_query(text: str) -> List[float]. The asymmetric design lets providers cache document embeddings while recomputing queries.

Q: What is text-embedding-3-small vs text-embedding-3-large?
A: OpenAI's third-generation embedding models. text-embedding-3-small has 1536 dimensions, text-embedding-3-large has 3072 dimensions. Both support a dimensions parameter to truncate embeddings for cheaper storage with minimal recall loss.

Q: What is Pinecone in LangChain?
A: Pinecone is a managed vector database. LangChain integrates it via langchain-pinecone: PineconeVectorStore.from_documents / from_texts and .as_retriever(). Supports metadata filtering and namespaces out of the box.

Q: What is Weaviate in LangChain?
A: Weaviate is an open-source vector database. langchain-weaviate exposes WeaviateVectorStore with hybrid (BM25 + vector) search and GraphQL filters. Good for self-hosted multi-tenant setups.

Q: What is Qdrant in LangChain?
A: Qdrant is a Rust-based vector store with built-in payload filtering, named vectors, and a managed cloud offering. langchain-qdrant provides QdrantVectorStore and supports as_retriever with filter kwargs.

Q: What is PGVector?
A: PGVector is the Postgres extension for vector search. langchain-postgres exposes PGVector — useful when you already have a Postgres database and want to avoid a second data store.

Q: What is InMemoryVectorStore?
A: InMemoryVectorStore (new in 0.2+) is a lightweight vector store in langchain-core that does exact nearest-neighbor search using numpy. Great for tests, prototypes, and small corpora — no extra dependencies.

Q: What is SQLDatabase in LangChain?
A: SQLDatabase wraps a SQLAlchemy engine and exposes .run, .get_table_info, and .get_usable_table_names. It is the base for the SQLDatabaseChain and the modern langchain.sql toolkit.

Q: What is the SQL Agent?
A: A LangChain/LangGraph agent that uses an LLM to write and execute SQL queries against a SQLDatabase. It inspects the schema, generates a query, executes it, and (with self-correction) retries on errors. Set top_k on SQLDatabase to cap rows returned.

Q: What is the Pandas DataFrame Agent?
A: An agent that operates on a pd.DataFrame by generating and executing Python code via a tool that runs in a PythonREPL. Use create_pandas_dataframe_agent. Enable only in sandboxed environments — arbitrary code execution is a security risk.

Q: What is the PythonREPL tool?
A: A PythonREPL tool executes arbitrary Python code in a subprocess. The agent emits code in a ```python``` block, the tool runs it, and returns stdout/stderr. Combine with create_python_agent for general compute tasks.

Q: What is RetrievalQA?
A: Legacy chain (RetrievalQA.from_chain_type) that wraps a retriever + LLM. In modern LangChain, you would build the same flow with LCEL using {"context": retriever, "question": RunnablePassthrough()} | prompt | llm | parser — equivalent but more flexible.

Q: What is ConversationalRetrievalChain?
A: Legacy chain that combines a retriever with a question-rewriting step that uses chat history to reformulate the query. Modern equivalent: a graph with a rewrite_question node feeding the retriever, then a generate node.

Q: What is create_stuff_documents_chain?
A: A chain that takes a list of Documents, stuffs them all into a single prompt (as {context}), and generates an answer. The "stuff" strategy is the simplest RAG approach — works only when total context fits the window.
from langchain.chains.combine_documents import create_stuff_documents_chain
chain = create_stuff_documents_chain(llm, prompt)

Q: What is create_map_reduce_documents_chain?
A: Generates an answer for each retrieved document independently, then summarizes all answers. Better than "stuff" for large corpora but slower (N+1 LLM calls) and loses cross-document reasoning.

Q: What is create_refine_documents_chain?
A: Iteratively refines a running answer as it processes documents sequentially. Each document is fed with the current answer to produce a refined version. Higher quality than map-reduce for long-form synthesis but linear in N.

Q: What is the Map-Rerank documents strategy?
A: For each document, ask the LLM to produce an answer and a confidence score; return the highest-scoring one. Cheaper than map-reduce, ignores cross-doc context.

Q: What is LongContextReorder?
A: A post-processor that rearranges retrieved documents so the most relevant are at the start and end of the list, exploiting the "lost in the middle" effect where LLMs pay more attention to the extremes of long contexts.

Q: What is EmbeddingsRedundantFilter?
A: A DocumentCompressor that drops retrieved documents whose embeddings are too similar to already-kept ones, removing near-duplicates from the context fed to the LLM.

Q: What is EmbeddingsFilter?
A: Like ContextualCompressionRetriever, but uses embeddings (not an LLM) to score each retrieved chunk against the query and keep only the top-K by similarity. Cheaper than LLM-based extraction.

Q: What is a Cross-Encoder Reranker?
A: A reranker model (e.g., CohereRerank, FlashrankRerank, CrossEncoderRerank) that scores each (query, document) pair jointly. It is more accurate than bi-encoder similarity but slower — typically applied to the top-K candidates from a fast retriever.

Q: How do you implement query rewriting in LCEL?
A: Add a RunnableLambda or prompt chain before the retriever that takes {question, chat_history} and emits a standalone search query. The output of this step becomes the retriever's input.
chain = (
    rewrite_prompt | llm | StrOutputParser() | retriever | ...
)

Q: What is the HyDE (Hypothetical Document Embeddings) technique?
A: You ask the LLM to generate a hypothetical answer to the query, embed that, and use the embedding for retrieval. Often gives better recall than embedding the bare question because it lands in the same region of embedding space as real answers.

Q: What is LangChain Evaluation?
A: LangChain integrates with LangSmith (and provides langchain.evaluation modules) for evaluating chains on datasets. Evaluators include StringEvaluator subclasses like QAEvaluator, CriteriaEvalChain, LabeledCriteriaEvalChain, and embedding-distance metrics.

Q: What is the StringEvaluator interface?
A: Subclasses implement _evaluate_strings (and optionally _aevaluate_strings) returning a dict with a "score" and optional "reasoning". Examples: exact match, regex match, embedding distance, LLM-judged criteria.

Q: What is CriteriaEvalChain?
A: An evaluator that asks an LLM to grade a prediction against a custom criterion (e.g., "Is the answer helpful? Concise? Factually correct?"). Useful for subjective quality metrics without writing a grader prompt from scratch.

Q: What is the embedding_distance metric?
A: Computes cosine, euclidean, or Manhattan distance between the embeddings of the prediction and the reference. Returns a float score. Cheaper than LLM-based evaluators but less semantically robust.

Q: What is a PairwiseStringEvaluator?
A: Compares two model outputs against each other (and optionally a reference) and returns a verdict: {"reasoning": ..., "value": "A"} or "B" or "tie". Used for head-to-head model comparisons.

Q: How do you write a custom evaluator?
A: Subclass StringEvaluator, implement _evaluate_strings, set requires_input, requires_reference, and evaluation_name. Return {"score": float, "reasoning": str}. Register it in your eval script.

Q: What is the LangChain caching system?
A: LangChain supports caching LLM responses to avoid repeat calls. Set set_llm_cache(InMemoryCache()) or SQLiteCache, RedisCache, RedisSemanticCache, or GPTCache. Caching is keyed by the serialized prompt + model params.

Q: What is RedisSemanticCache?
A: A semantic cache that uses embedding similarity to match new queries to cached prompts. Two prompts with similar meaning (not just identical text) return the same cached response. Set a score_threshold to control how strict the match is.