How to Build a policy Q&A Agent Using LangChain in TypeScript for lending

By Cyprian AaronsUpdated 2026-04-21
policy-q-alangchaintypescriptlendingpolicy-qanda

A policy Q&A agent for lending answers questions like “Can we approve this borrower under current DTI rules?” or “What documents are required for self-employed applicants?” It matters because lending teams need fast, consistent answers grounded in policy, not guesses from a chat model. Done right, this reduces underwriting delays, keeps responses auditable, and lowers compliance risk.

Architecture

  • Policy source loader

    • Pulls approved lending policies from PDFs, SharePoint, Confluence, or a document store.
    • Only indexed content should be visible to the agent.
  • Chunking and embeddings pipeline

    • Splits policy docs into retrievable chunks.
    • Uses embeddings to support semantic search over policy language.
  • Vector store

    • Stores policy chunks with metadata like policy_version, jurisdiction, effective_date, and doc_type.
    • Enables filtering by product line or region.
  • Retriever

    • Fetches the most relevant policy passages for each question.
    • Should support metadata filters for lending scope control.
  • LLM answer chain

    • Generates an answer only from retrieved context.
    • Must cite sources and refuse when evidence is missing.
  • Audit and observability layer

    • Logs question, retrieved documents, answer, and policy version.
    • Required for compliance review and dispute resolution.

Implementation

1) Load policies and build a vector index

Use LangChain’s document loaders, text splitter, embeddings, and vector store. In lending, keep metadata attached so you can filter by jurisdiction or product later.

import { PDFLoader } from "@langchain/community/document_loaders/fs/pdf";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";

async function buildIndex() {
  const loader = new PDFLoader("./policies/lending-policy.pdf");
  const docs = await loader.load();

  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize: 900,
    chunkOverlap: 150,
  });

  const splitDocs = await splitter.splitDocuments(
    docs.map((d) => ({
      ...d,
      metadata: {
        ...d.metadata,
        policy_version: "2025.01",
        jurisdiction: "US",
        doc_type: "lending_policy",
      },
    }))
  );

  const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });
  const vectorStore = await MemoryVectorStore.fromDocuments(splitDocs, embeddings);

  return vectorStore;
}

For production, replace MemoryVectorStore with a persistent store like Pinecone or pgvector. The pattern stays the same; only the storage backend changes.

2) Create a retriever with lending filters

The agent should not search across all policies blindly. Filter by jurisdiction or loan product so a mortgage question does not pull in consumer installment guidance.

import { VectorStoreRetriever } from "@langchain/core/vectorstores";

async function getRetriever(vectorStore: any) {
  return vectorStore.asRetriever({
    k: 4,
    filter: {
      jurisdiction: "US",
      doc_type: "lending_policy",
    },
  }) as VectorStoreRetriever;
}

If your store supports richer filtering, add effective_date and product_line. That gives you deterministic scope control during audits.

3) Build a grounded Q&A chain

Use ChatOpenAI with RunnableSequence so the model answers only from retrieved context. The prompt should force citations and allow refusal when policy evidence is absent.

import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";

const prompt = PromptTemplate.fromTemplate(`
You are a lending policy assistant.
Answer only using the provided policy context.
If the context does not contain the answer, say "I couldn't find that in the current policy set."

Policy context:
{context}

Question:
{question}

Return:
1. Direct answer
2. Policy citations with document names or metadata
3. Any caveats for compliance review
`);

function formatDocs(docs: any[]) {
  return docs
    .map(
      (doc) =>
        `[${doc.metadata.policy_version} | ${doc.metadata.jurisdiction}] ${doc.pageContent}`
    )
    .join("\n\n---\n\n");
}

export async function createQAChain(retriever: any) {
  const llm = new ChatOpenAI({
    model: "gpt-4o-mini",
    temperature: 0,
  });

  return RunnableSequence.from([
    {
      question: new RunnablePassthrough(),
      context: async (question: string) => {
        const docs = await retriever.invoke(question);
        return formatDocs(docs);
      },
    },
    prompt,
    llm,
  ]);
}

This is the core pattern. Retrieval happens first, then the model sees only relevant policy text, which is what you want in regulated lending workflows.

4) Invoke the agent and log the result

Keep an audit trail of what was asked and what sources were used. That gives compliance teams something concrete to review later.

async function main() {
  const vectorStore = await buildIndex();
  const retriever = await getRetriever(vectorStore);
  const chain = await createQAChain(retriever);

  const question =
    "Can we approve an applicant with DTI above our standard threshold if they have compensating factors?";

  const response = await chain.invoke(question);

  console.log(String(response));
}

main().catch(console.error);

In production, persist:

  • user ID or service account
  • timestamp
  • question text
  • retrieved document IDs
  • policy version
  • final answer

That record is what makes the system defensible during model risk reviews.

Production Considerations

  • Deployment

    • Host the vector store in-region if your lending policies or customer data have residency constraints.
    • Keep raw borrower data out of the retrieval index unless you have a clear legal basis and retention rule.
  • Monitoring

    • Track retrieval hit rate, refusal rate, and citation coverage.
    • Alert when answers are produced without supporting chunks or when outdated policy versions dominate retrieval.
  • Guardrails

    • Enforce a “no evidence, no answer” rule.
    • Add allowlisted topics so the agent answers policy questions only, not underwriting decisions or legal advice.
  • Auditability

    • Store prompt inputs, retrieved chunks, model version, and output hash.
    • Make it easy to reconstruct why an answer was returned for examiners or internal audit.

Common Pitfalls

  1. Using the LLM as a free-form advisor

    • Mistake: letting it answer without retrieval.
    • Fix: require retriever output before every response and refuse when nothing relevant is found.
  2. Ignoring policy versioning

    • Mistake: mixing old and current policies in one index without metadata filters.
    • Fix: tag every chunk with effective_date, version, and jurisdiction, then filter at query time.
  3. Treating borrower data like normal chat input

    • Mistake: indexing PII or sensitive loan files without controls.
    • Fix: separate customer data from policy knowledge bases, minimize retention, and apply residency controls where required.

If you want this agent to survive real lending operations, keep it narrow. Answer only from approved policies, log everything important, and make refusal a first-class behavior rather than an error case.


Keep learning

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

Related Guides