How to Build a policy Q&A Agent Using LangGraph in Python for pension funds
A policy Q&A agent for pension funds answers member, trustee, and operations questions against approved policy documents, scheme rules, investment guidelines, and service procedures. It matters because these teams need fast answers without letting the model invent policy, leak sensitive data, or bypass compliance controls.
Architecture
- •
User interface layer
- •Chat UI, internal portal widget, or support console.
- •Passes user role and jurisdiction with every request.
- •
Policy retrieval layer
- •Vector search over approved documents: scheme rules, contribution policies, retirement options, complaint handling, AML/KYC procedures.
- •Filters by fund, region, document version, and effective date.
- •
LangGraph orchestration
- •A stateful graph that routes between retrieval, answer drafting, compliance checks, and escalation.
- •Keeps the workflow deterministic enough for audit.
- •
Guardrail and policy engine
- •Checks for prohibited outputs: legal advice, unsupported claims, personal data exposure.
- •Forces refusal or human handoff when confidence is low.
- •
Audit logging
- •Stores question, retrieved sources, answer version, model ID, and decision path.
- •Required for trustee review and regulatory traceability.
- •
Human escalation path
- •Routes ambiguous cases to a pension administrator or compliance officer.
- •Needed for benefit calculations, exceptions, disputes, and complaints.
Implementation
1) Define state and load your approved policy index
Use a small graph state object that carries the user query, retrieved context, draft answer, and final decision. For pension funds, keep metadata on document version and jurisdiction so you can prove which policy was used.
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END
from langchain_core.documents import Document
from langchain_core.messages import HumanMessage
from operator import add
class QAState(TypedDict):
question: str
jurisdiction: str
user_role: str
docs: list[Document]
answer: str
escalate: bool
def retrieve_policy_docs(state: QAState) -> QAState:
# Replace this with your vector store retrieval filtered by fund/jurisdiction/version.
docs = [
Document(
page_content="Members may retire from age 55 subject to scheme rules.",
metadata={"source": "scheme_rules_v4.pdf", "jurisdiction": state["jurisdiction"]}
)
]
return {"docs": docs}
def draft_answer(state: QAState) -> QAState:
context = "\n".join(d.page_content for d in state["docs"])
answer = f"Based on approved policy: {context}"
return {"answer": answer}
graph = StateGraph(QAState)
graph.add_node("retrieve_policy_docs", retrieve_policy_docs)
graph.add_node("draft_answer", draft_answer)
graph.add_edge(START, "retrieve_policy_docs")
graph.add_edge("retrieve_policy_docs", "draft_answer")
graph.add_edge("draft_answer", END)
app = graph.compile()
2) Add a compliance gate before answering
This is where LangGraph earns its keep. Use add_conditional_edges to route low-confidence or high-risk questions to a human review path instead of letting the model guess.
def compliance_check(state: QAState) -> str:
question = state["question"].lower()
risky_terms = [
"guarantee", "legal advice", "transfer value",
"complaint", "tax treatment", "early retirement"
]
if any(term in question for term in risky_terms):
return "escalate"
if not state.get("docs"):
return "escalate"
return "answer"
def escalate_to_human(state: QAState) -> QAState:
return {
"answer": (
"I can't provide a definitive policy answer from the approved sources. "
"This case has been sent to a pension administrator for review."
),
"escalate": True,
}
workflow = StateGraph(QAState)
workflow.add_node("retrieve_policy_docs", retrieve_policy_docs)
workflow.add_node("draft_answer", draft_answer)
workflow.add_node("escalate_to_human", escalate_to_human)
workflow.add_edge(START, "retrieve_policy_docs")
workflow.add_conditional_edges(
"retrieve_policy_docs",
compliance_check,
{
"answer": "draft_answer",
"escalate": "escalate_to_human",
},
)
workflow.add_edge("draft_answer", END)
workflow.add_edge("escalate_to_human", END)
app = workflow.compile()
3) Run the graph with audit-friendly inputs
For production use in pension administration, always pass role and jurisdiction into the state. That gives you a clean audit trail and lets retrieval respect residency constraints.
result = app.invoke({
"question": "Can I retire at 54 under this scheme?",
"jurisdiction": "UK",
"user_role": "member_services",
})
print(result["answer"])
print("Escalated:", result["escalate"])
If you want more control over the answer generation step, swap draft_answer for an LLM node using ChatOpenAI or another chat model from LangChain. Keep the prompt tight:
- •only use retrieved documents
- •cite source names
- •refuse unsupported claims
- •escalate ambiguous benefit questions
4) Add source citation and refusal behavior
Pension funds need traceability. Don’t return free-form answers without citations; attach document names and effective dates so reviewers can verify them quickly.
def draft_answer(state: QAState) -> QAState:
if not state["docs"]:
return {
"answer": (
"No approved policy source was found for this question. "
"Please route it to the pensions team."
),
"escalate": True,
}
doc = state["docs"][0]
answer = (
f"According to {doc.metadata['source']} "
f"(jurisdiction: {doc.metadata['jurisdiction']}), "
f"{doc.page_content}"
)
return {"answer": answer}
Production Considerations
- •
Data residency
- •Keep member data and indexed policy content in-region.
- •If your fund operates across jurisdictions, partition indexes by country to avoid cross-border leakage.
- •
Audit logging
- •Log prompt inputs, retrieved document IDs, model version, graph path taken, and final output.
- •Store logs immutably so trustees can review decisions during complaints or regulatory audits.
- •
Guardrails
- •Block outputs that look like financial advice or benefit guarantees.
- •Escalate anything involving tax treatment, transfer values, protected benefits, divorce orders, or exceptions to scheme rules.
- •
Monitoring
- •Track escalation rate, refusal rate, retrieval hit rate, and citation coverage.
- •A rising fallback rate usually means your policy corpus is stale or your chunking strategy is poor.
Common Pitfalls
- •
Letting the model answer without grounded sources
- •Fix it by making retrieval mandatory before drafting.
- •If no approved source is returned, refuse or escalate.
- •
Ignoring scheme versioning
- •Pension policies change often: retirement age rules are not static.
- •Store effective dates in metadata and filter retrieval by active version only.
- •
Treating all users the same
- •A member asking about benefits should not see internal admin procedures.
- •Route by
user_roleand restrict what context each role can retrieve.
- •
Skipping human review for edge cases
- •Benefit disputes and exception handling are not chatbot territory.
- •Use LangGraph branching so high-risk queries land with a pensions specialist before anything goes back to the user.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit