Unit 06.02: Returning sources alongside the answer
An answer without its sources is an answer nobody can check. Returning both requires the chain to branch and rejoin.
Retrieve once, use twice
RunnableParallel runs branches side by side. The retrieved documents feed both the answer and the source list, so retrieval happens once.
The code returns an answer with its source metadata.
from langchain_core.documents import Document
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.language_models.fake_chat_models import FakeListChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda, RunnableParallel
from langchain_core.vectorstores import InMemoryVectorStore
store = InMemoryVectorStore.from_documents(
[Document("Refunds are allowed within 7 days.",
metadata={"id": "c1", "source": "support-policies-v4.md",
"updated": "2026-06-14"})],
DeterministicFakeEmbedding(size=64))
retriever = store.as_retriever(search_kwargs={"k": 1})
prompt = ChatPromptTemplate.from_messages([("human", "{context}\n\n{question}")])
model = FakeListChatModel(responses=["Refunds are allowed within 7 days. [c1]"])
def build(inputs):
docs = inputs["docs"]
context = "\n".join(f"[{d.metadata['id']}] {d.page_content}" for d in docs)
return {"context": context, "question": inputs["question"]}
chain = (RunnableParallel(docs=retriever, question=lambda q: q)
| RunnableParallel(
answer=RunnableLambda(build) | prompt | model | StrOutputParser(),
sources=lambda x: [d.metadata for d in x["docs"]]))
out = chain.invoke("What is the refund window?")
print("answer :", out["answer"])
for s in out["sources"]:
print(f"source : {s['id']} in {s['source']} (updated {s['updated']})")
# Returning the metadata alongside the answer is what makes the citation
# followable and lets the caller surface the date -- which is how a user judges
# whether an answer is current.
The sources carry updated alongside the id. That date is what lets a caller surface how current the answer is, and it is how a user judges whether to trust it - the single most useful field to expose and the one most often dropped.
Retrieving once matters at volume. A naive version calls the retriever twice, once for the answer and once for the sources, and can return sources that differ from what the model actually saw.
The mistake this prevents
The mistake is reconstructing the sources by parsing the citations out of the answer text. The model may cite something it was not given, may omit a citation, or may format it unexpectedly - and you would be reporting what it claimed rather than what it received.
Takeaway
Branch with RunnableParallel so retrieval runs once and feeds both the answer and the source list. Report what was retrieved, not what the answer claims to cite.
