手把手elasticsearch学习之构建金融交易审批 HITL 系统
一、先搞懂:金融场景为什么用HITL?核心逻辑与流程
1. 金融交易审批的核心痛点(HITL完美解决)
金融交易审批是强监管、零容错场景,既不能全靠人工(效率低、标准不统一),更不能让AI全自动审批(监管不允许、风险不可控):
-
监管要求:大额交易、高风险交易必须有人工复核留痕,不能纯自动化
-
风险控制:AI无法处理规则边界模糊、信息缺失、突发风险的场景,必须人工介入
-
效率平衡:低风险、标准化交易全自动审批,释放人力;中高风险交易精准触发人工干预
2. HITL核心设计原则
只在3种场景触发人工干预,绝不无效打断:
-
中高风险交易(大额、公转私、敏感地区/行业),必须人工复核
-
交易信息缺失/用途模糊,需要申请人补充材料
-
规则边界模糊(无匹配历史案例、监管新规无明确指引),需要风控人员人工判定
3. 全流程业务全貌(先懂逻辑,再写代码)
申请人提交交易申请 → 1.ES检索匹配【历史审批案例+风控规则+监管要求】
→ 2.AI生成初审意见&风险评级 → 3.条件分支判断
├─ 低风险(小额、白名单、历史100%合规):自动审批通过 → 流程结束
├─ 规则边界模糊/信息缺失:【HITL2:暂停,让申请人补充材料】→ 回到步骤2重审
└─ 中高风险交易:【HITL1:暂停,提交风控审批员人工复核】
├─ 审批员驳回:直接生成驳回通知 → 流程结束
├─ 审批员要求补充材料:【HITL2:暂停,让申请人补材料】→ 回到步骤2重审
└─ 审批员通过:生成最终审批通过结论 → 流程结束
4. 核心组件分工
-
Elasticsearch 9.x:存储历史审批案例、风控规则库、监管政策、黑白名单,做语义+规则混合检索,给AI提供精准的审批依据,避免幻觉
-
LangGraph:实现审批工作流的编排、状态全链路留存、中断恢复(HITL的核心),所有审批节点全留痕,符合监管审计要求
-
HITL中断机制:精准触发人工介入,完整保存审批进度,人工输入后从断点继续运行,全程可追溯
二、环境准备(复用之前的ES9.x环境,无需额外配置)
前置要求
-
已按之前的教程完成Elasticsearch 9.x Docker本地部署,有可用的ES API Key
-
Node.js 18+、Docker Desktop正常运行
-
可用的OpenAI API Key
-
新建空文件夹(比如
es-hitl-finance-approval),作为项目目录
快速初始化项目
-
项目目录下打开终端,执行初始化:
npm init -y -
安装固定版本依赖(和之前完全兼容,避免版本冲突):
# 核心依赖 npm install @elastic/elasticsearch@8.15.0 @langchain/community@0.3.10 @langchain/core@0.3.15 @langchain/langgraph@0.2.18 @langchain/openai@0.3.11 dotenv@16.4.5 --legacy-peer-deps # 开发依赖 npm install --save-dev tsx@4.19.1 typescript@5.6.3 -
新建
tsconfig.json,直接复制:{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "node", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "outDir": "./dist" }, "include": ["*.ts"], "exclude": ["node_modules"] } -
新建
.env环境变量文件,替换成你自己的信息:ELASTICSEARCH_ENDPOINT=https://localhost:9200 ELASTICSEARCH_API_KEY=你的ES API Key OPENAI_API_KEY=你的OpenAI API Key
三、核心代码实现:全流程贴合金融审批业务
我们依然分为2个核心文件,完全适配金融场景,每一行代码都有原理说明:
-
dataIngestion.ts:ES索引创建、金融审批数据集定义、数据批量摄入 -
main.ts:审批工作流全流程实现、HITL中断处理、节点逻辑
文件1:dataIngestion.ts 金融数据摄入模块
核心作用
给ES创建审批专用索引,定义金融数据结构,导入历史审批案例、风控规则、监管要求,为AI审批提供精准的检索依据,所有字段完全贴合金融审批业务。
完整代码(直接复制)
import { Client } from "@elastic/elasticsearch";
import { OpenAIEmbeddings } from "@langchain/openai";
import dotenv from "dotenv";
dotenv.config();
// ===================== 1. 初始化客户端 =====================
// ES客户端,适配9.x本地部署
export const esClient = new Client({
node: process.env.ELASTICSEARCH_ENDPOINT || "https://localhost:9200",
auth: { apiKey: process.env.ELASTICSEARCH_API_KEY || "" },
tls: { rejectUnauthorized: false }, // 本地开发必加,解决自签名证书问题
});
// Embedding客户端,生成向量用于语义检索
export const embeddings = new OpenAIEmbeddings({
model: "text-embedding-3-small",
});
// 固定索引名称,全流程统一
export const APPROVAL_INDEX = "finance-trade-approval";
// ===================== 2. 金融审批数据类型定义 =====================
// 审批文档结构,和ES Mapping一一对应
export interface ApprovalDocument {
pageContent: string; // 完整的审批内容/规则描述,用于生成向量检索
metadata: {
docId: string; // 唯一ID
docType: "approval_case" | "risk_rule" | "regulatory_policy"; // 文档类型:审批案例/风控规则/监管政策
riskLevel: "low" | "medium" | "high"; // 风险等级
tradeScene?: string; // 交易场景(对公转账/公转私/货款/服务费等)
tradeAmount?: string; // 交易金额区间
approvalResult?: "approved" | "rejected" | "supplement"; // 审批结果
coreRule?: string; // 风控核心规则
approvalBasis?: string; // 审批依据
keyTerms?: string; // 关键词
};
vector?: number[]; // 向量字段,用于语义检索
}
// ===================== 3. ES 9.x 索引Mapping定义(金融场景专属) =====================
const INDEX_MAPPING = {
mappings: {
properties: {
pageContent: { type: "text" }, // 全文检索+向量生成
metadata: {
properties: {
docId: { type: "keyword" },
docType: { type: "keyword" },
riskLevel: { type: "keyword" },
tradeScene: { type: "keyword" },
tradeAmount: { type: "keyword" },
approvalResult: { type: "keyword" },
coreRule: { type: "text" },
approvalBasis: { type: "text" },
keyTerms: { type: "text" },
},
},
// 向量字段,适配OpenAI embedding 1536维,ES9.x专属优化
vector: {
type: "dense_vector",
dims: 1536,
index: true,
similarity: "cosine",
},
},
},
};
// ===================== 4. 金融审批核心数据集(可无限扩展) =====================
// 包含:历史审批案例、风控规则、监管政策,完全贴合真实企业审批场景
export const FINANCE_APPROVAL_DATA: ApprovalDocument[] = [
// ------------ 历史审批案例 ------------
{
pageContent: "对公转账审批案例:付款方为XX科技有限公司,收款方为XX供应链有限公司,交易金额35万元,交易用途为原材料货款,附言匹配合同编号,双方为长期合作白名单客户,历史交易均合规无风险。审批结果:自动通过,无需人工复核。",
metadata: {
docId: "CASE-001",
docType: "approval_case",
riskLevel: "low",
tradeScene: "对公转账-货款",
tradeAmount: "10万-50万",
approvalResult: "approved",
approvalBasis: "白名单客户、交易用途清晰、有合同支撑、历史合规",
keyTerms: "对公转账、货款、白名单、低风险",
}
},
{
pageContent: "公转私审批案例:付款方为XX贸易有限公司,收款方为个人张三,交易金额85万元,交易用途为备用金,无任何合同、发票支撑,无历史合作记录。风险判定:高风险,涉嫌公转私套现、偷逃税款。审批结果:人工复核后驳回,要求补充完整的交易凭证、个税缴纳证明。",
metadata: {
docId: "CASE-002",
docType: "approval_case",
riskLevel: "high",
tradeScene: "公转私-备用金",
tradeAmount: "50万以上",
approvalResult: "rejected",
approvalBasis: "大额公转私、用途模糊、无凭证支撑、无历史交易",
keyTerms: "公转私、大额、无凭证、高风险",
}
},
{
pageContent: "对公转账审批案例:付款方为XX建筑有限公司,收款方为XX建材经营部,交易金额120万元,交易用途为工程款,附言无合同编号,收款方为首次合作,注册地为敏感地区。风险判定:中高风险,涉嫌虚假交易、洗钱。审批结果:暂停,要求补充采购合同、发票、收货凭证,人工复核后再审批。",
metadata: {
docId: "CASE-003",
docType: "approval_case",
riskLevel: "medium",
tradeScene: "对公转账-工程款",
tradeAmount: "100万以上",
approvalResult: "supplement",
approvalBasis: "首次合作、大额交易、敏感地区、无合同支撑",
keyTerms: "大额、首次合作、敏感地区、补充材料",
}
},
{
pageContent: "重复交易审批案例:付款方XX电商有限公司,收款方XX物流有限公司,交易金额8万元,交易用途为月度物流服务费,双方为固定月度合作,历史12个月均有相同金额、相同用途的交易,全部合规。审批结果:自动通过,无需人工复核。",
metadata: {
docId: "CASE-004",
docType: "approval_case",
riskLevel: "low",
tradeScene: "对公转账-服务费",
tradeAmount: "10万以下",
approvalResult: "approved",
approvalBasis: "固定周期重复交易、历史100%合规、用途清晰",
keyTerms: "重复交易、小额、合规、自动通过",
}
},
// ------------ 风控规则库 ------------
{
pageContent: "企业对公交易风控规则:单笔交易金额≥50万元的对公转账,无论交易场景,必须提交风控人员人工复核,不得自动审批。",
metadata: {
docId: "RULE-001",
docType: "risk_rule",
riskLevel: "medium",
coreRule: "单笔≥50万对公交易,强制人工复核",
keyTerms: "大额交易、人工复核、强制规则",
}
},
{
pageContent: "企业公转私交易风控规则:单笔交易金额≥20万元的公转私转账,无论用途,必须提交风控人员人工复核,同时需提供交易合同、个税缴纳证明、发票等支撑材料,否则直接驳回。",
metadata: {
docId: "RULE-002",
docType: "risk_rule",
riskLevel: "high",
coreRule: "单笔≥20万公转私,强制人工复核+材料校验",
keyTerms: "公转私、大额、强制复核、材料要求",
}
},
{
pageContent: "企业交易风控黑名单规则:收款方注册地为监管敏感地区、收款方在涉洗钱/涉诈黑名单内的交易,无论金额大小,直接冻结交易,提交风控人工审核,不得自动通过。",
metadata: {
docId: "RULE-003",
docType: "risk_rule",
riskLevel: "high",
coreRule: "敏感地区/黑名单交易,强制人工审核",
keyTerms: "黑名单、敏感地区、冻结、人工审核",
}
},
{
pageContent: "企业交易自动审批规则:同时满足以下条件的交易,可自动审批通过:1.单笔金额<10万元;2.交易双方为合作超6个月的白名单客户;3.交易用途与历史合规交易一致;4.收款方不在黑名单内。",
metadata: {
docId: "RULE-004",
docType: "risk_rule",
riskLevel: "low",
coreRule: "小额白名单合规交易,可自动通过",
keyTerms: "自动审批、小额、白名单、合规",
}
},
// ------------ 监管政策要求 ------------
{
pageContent: "《金融机构大额交易和可疑交易报告管理办法》规定:企业对公账户单笔或者当日累计人民币交易200万元以上、个人账户单笔或者当日累计人民币交易50万元以上的跨境交易,应当向中国反洗钱监测分析中心提交大额交易报告,交易审批必须留存完整的人工复核记录。",
metadata: {
docId: "POLICY-001",
docType: "regulatory_policy",
riskLevel: "high",
coreRule: "大额交易必须人工复核、留存审计记录",
keyTerms: "反洗钱、大额交易、监管要求、审计留痕",
}
},
{
pageContent:《人民币银行结算账户管理办法》规定:严禁企业利用对公账户向个人账户无合理用途转账,公转私交易必须有真实的交易背景、合法的支付依据,否则银行有权拒绝办理,企业需留存完整的交易凭证备查。",
metadata: {
docId: "POLICY-002",
docType: "regulatory_policy",
riskLevel: "high",
coreRule: "公转私必须有真实交易背景和合法依据",
keyTerms: "公转私、账户管理、监管要求、交易凭证",
}
}
];
// ===================== 5. 数据摄入主函数 =====================
export async function ingestApprovalData() {
console.log("🔄 Starting finance approval data ingestion to ES 9.x...");
// 开发环境:索引已存在则删除,避免重复数据
if (await esClient.indices.exists({ index: APPROVAL_INDEX })) {
await esClient.indices.delete({ index: APPROVAL_INDEX });
console.log(`🗑️ Deleted existing index: ${APPROVAL_INDEX}`);
}
// 创建索引,应用金融场景专属Mapping
await esClient.indices.create({
index: APPROVAL_INDEX,
body: INDEX_MAPPING,
});
console.log(`✅ Created index: ${APPROVAL_INDEX} with ES 9.x finance mapping`);
// 批量生成向量+导入ES
const operations: any[] = [];
for (const doc of FINANCE_APPROVAL_DATA) {
// 给文档内容生成向量,用于语义检索
const vector = await embeddings.embedQuery(doc.pageContent);
operations.push(
{ index: { _index: APPROVAL_INDEX } },
{ ...doc, vector }
);
}
// 批量写入ES,refresh=true强制刷新,确保写入后立即可检索
await esClient.bulk({ refresh: true, operations });
console.log(`✅ Successfully ingested ${FINANCE_APPROVAL_DATA.length} finance approval documents`);
console.log("🎉 Finance data ingestion completed!\n");
}
文件2:main.ts 金融交易审批HITL工作流核心代码
完全贴合真实审批流程,2个核心HITL节点(风控员人工复核、申请人补充材料),全链路状态留痕,符合监管审计要求,每一个节点都有原理说明。
完整代码(直接复制)
import { StateGraph, Annotation, interrupt, MemorySaver } from "@langchain/langgraph";
import { Command } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import { ElasticVectorSearch } from "@langchain/community/vectorstores/elasticsearch";
import * as readline from "readline";
import dotenv from "dotenv";
// 从dataIngestion导入预定义的内容
import { ingestApprovalData, APPROVAL_INDEX, esClient, embeddings, ApprovalDocument } from "./dataIngestion";
dotenv.config();
// ===================== 1. 初始化客户端与工具函数 =====================
// 1.1 初始化LLM,temperature=0 确保输出稳定、无幻觉,符合金融场景严谨性要求
const llm = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
// 1.2 初始化ES向量存储,用于审批案例/规则/政策的语义检索
const vectorStore = new ElasticVectorSearch(embeddings, {
client: esClient,
indexName: APPROVAL_INDEX,
});
// 1.3 终端用户输入工具函数,用于HITL环节读取人工输入
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function getUserInput(prompt: string): Promise<string> {
return new Promise((resolve) => rl.question(prompt, resolve));
}
// ===================== 2. 审批全流程全局状态定义【核心】 =====================
// 相当于审批流程的「共享档案袋」,所有节点都能读写,全链路留痕,符合监管审计要求
export const TradeApprovalState = Annotation.Root({
// 申请人提交的交易申请核心信息
tradeApplication: Annotation<{
payerName: string; // 付款方名称
payeeName: string; // 收款方名称
payeeType: "enterprise" | "individual"; // 收款方类型:企业/个人
tradeAmount: number; // 交易金额(元)
tradePurpose: string; // 交易用途
contractNo?: string; // 合同编号
isLongTermPartner: boolean; // 是否长期合作方
payeeAddress?: string; // 收款方注册地
}>(),
// ES检索到的匹配案例、规则、政策
matchedDocuments: Annotation<ApprovalDocument[]>(),
// AI生成的初审意见
preliminaryReview: Annotation<{
riskLevel: "low" | "medium" | "high";
reviewOpinion: string;
needManualReview: boolean; // 是否需要人工复核
needSupplement: boolean; // 是否需要补充材料
missingMaterials: string[]; // 缺失的材料列表
} | null>(),
// 【HITL1】风控审批员的人工复核意见
manualReviewOpinion: Annotation<{
reviewResult: "approved" | "rejected" | "supplement";
reviewComment: string;
} | null>(),
// 【HITL2】申请人补充的材料信息
supplementMaterial: Annotation<string>(),
// 最终审批结论
finalApprovalResult: Annotation<{
result: "approved" | "rejected";
finalOpinion: string;
auditTrail: string[]; // 审批全链路审计留痕
} | null>(),
});
// ===================== 3. 审批流程节点实现 =====================
// 每个节点对应审批流程的一个环节,输入当前状态,输出要更新到状态里的内容,全链路留痕
// -------------------- 节点1:检索匹配审批依据 --------------------
// 原理:把交易申请转成向量,从ES里检索最匹配的历史案例、风控规则、监管政策,给AI初审提供100%准确的依据,杜绝幻觉
async function searchApprovalBasis(state: typeof TradeApprovalState.State) {
const trade = state.tradeApplication;
console.log("📑 Received trade application:");
console.log(` Payer: ${trade.payerName} | Payee: ${trade.payeeName} (${trade.payeeType})`);
console.log(` Amount: ¥${trade.tradeAmount} | Purpose: ${trade.tradePurpose}\n`);
console.log("🔍 Searching for matched approval cases, risk rules and regulatory policies...");
// 生成检索query,把交易核心信息拼成自然语言,精准匹配
const searchQuery = `
${trade.payeeType === "individual" ? "公转私" : "对公转账"}交易,
金额${trade.tradeAmount}元,用途${trade.tradePurpose},
${trade.isLongTermPartner ? "长期合作方" : "首次合作方"}
`;
// 相似度检索,最多返回8条最匹配的内容,覆盖案例、规则、政策
const results = await vectorStore.similaritySearch(searchQuery, 8);
const matchedDocuments = results.map((d) => d as unknown as ApprovalDocument);
// 打印匹配到的内容,全流程透明
console.log(`\n✅ Found ${matchedDocuments.length} matched documents:\n`);
matchedDocuments.forEach((doc, index) => {
console.log(`${index + 1}. [${doc.metadata.docType}] ${doc.metadata.docId} | Risk Level: ${doc.metadata.riskLevel}`);
console.log(` Core Content: ${doc.pageContent.substring(0, 100)}...\n`);
});
return { matchedDocuments };
}
// -------------------- 节点2:AI生成初审意见&风险评级 --------------------
// 原理:基于ES检索到的准确依据,AI做初审,判定风险等级,明确是否需要人工复核、是否需要补充材料,用结构化输出强制格式,杜绝幻觉
async function generatePreliminaryReview(state: typeof TradeApprovalState.State) {
console.log("📝 Generating preliminary review opinion...\n");
const trade = state.tradeApplication;
const matchedDocs = state.matchedDocuments || [];
// 把匹配到的文档拼成上下文,给LLM做依据
const docsContext = matchedDocs
.map((doc, i) => `${i + 1}. ${doc.metadata.docType.toUpperCase()}: ${doc.pageContent}`)
.join("\n\n");
// 【金融场景核心】强制结构化输出,LLM必须严格按照格式返回,不能自由发挥
const structuredLlm = llm.withStructuredOutput({
name: "trade_approval_preliminary_review",
schema: {
type: "object",
properties: {
riskLevel: {
type: "string",
enum: ["low", "medium", "high"],
description: "Comprehensive risk level of the trade",
},
reviewOpinion: {
type: "string",
description: "Detailed preliminary review opinion, based on the matched rules and cases",
},
needManualReview: {
type: "boolean",
description: "Whether the trade needs manual review by risk controller",
},
needSupplement: {
type: "boolean",
description: "Whether the applicant needs to supplement materials",
},
missingMaterials: {
type: "array",
items: { type: "string" },
description: "List of missing materials, empty if no supplement needed",
},
},
required: ["riskLevel", "reviewOpinion", "needManualReview", "needSupplement", "missingMaterials"],
},
});
// 初审提示词,严格贴合金融审批逻辑,要求必须基于提供的依据,不能编造
const prompt = `
You are a professional financial trade approval specialist. Based on the trade application, matched approval cases, risk rules and regulatory policies, generate a preliminary review opinion.
TRADE APPLICATION:
- Payer: ${trade.payerName}
- Payee: ${trade.payeeName} (${trade.payeeType})
- Trade Amount: ¥${trade.tradeAmount}
- Trade Purpose: ${trade.tradePurpose}
- Contract No.: ${trade.contractNo || "Not provided"}
- Is Long-term Partner: ${trade.isLongTermPartner ? "Yes" : "No"}
- Payee Address: ${trade.payeeAddress || "Not provided"}
MATCHED APPROVAL BASIS (cases, rules, policies):
${docsContext}
REQUIREMENTS:
1. You must strictly base your judgment on the provided matched basis, do not make up any rules.
2. Accurately judge the risk level, and clearly state whether manual review is needed.
3. If the trade information is incomplete, clearly list the missing materials.
4. The opinion must be professional, rigorous, and in line with financial regulatory requirements.
`;
const response = await structuredLlm.invoke([
{
role: "system",
content: "You are a rigorous financial trade approval specialist, only output content according to the required format.",
},
{ role: "user", content: prompt },
]);
// 打印初审意见,全流程透明
console.log("✅ Preliminary Review Completed:");
console.log(` Risk Level: ${response.riskLevel.toUpperCase()}`);
console.log(` Need Manual Review: ${response.needManualReview ? "YES" : "NO"}`);
console.log(` Need Supplement Materials: ${response.needSupplement ? "YES" : "NO"}`);
console.log(` Review Opinion: ${response.reviewOpinion}\n`);
if (response.missingMaterials.length > 0) {
console.log(" Missing Materials:");
response.missingMaterials.forEach((item: string, i: number) => {
console.log(` ${i + 1}. ${item}`);
});
console.log("\n");
}
return { preliminaryReview: response };
}
// -------------------- 节点3:【HITL1】风控审批员人工复核 --------------------
// 原理:只有AI判定需要人工复核的中高风险交易,才会触发这个节点,工作流暂停,等待风控员输入复核意见,符合监管要求
function manualReview(state: typeof TradeApprovalState.State) {
const review = state.preliminaryReview;
const trade = state.tradeApplication;
if (!review) return {};
console.log("\n⚖️ HITL #1: RISK CONTROLLER MANUAL REVIEW REQUIRED\n");
console.log("=".repeat(80));
console.log("TRADE DETAILS FOR REVIEW:");
console.log(`Payer: ${trade.payerName} | Payee: ${trade.payeeName} (${trade.payeeType})`);
console.log(`Amount: ¥${trade.tradeAmount} | Purpose: ${trade.tradePurpose}`);
console.log(`\nPRELIMINARY REVIEW OPINION:`);
console.log(`Risk Level: ${review.riskLevel.toUpperCase()}`);
console.log(`Opinion: ${review.reviewOpinion}`);
if (review.missingMaterials.length > 0) {
console.log(`Missing Materials: ${review.missingMaterials.join(", ")}`);
}
console.log("=".repeat(80) + "\n");
// 中断工作流,等待风控员输入复核意见,完整保存当前状态
const result = interrupt({
question: "👮♂️ Risk Controller, please enter your review result (approved/rejected/supplement) + comment: ",
});
// 解析风控员的输入,结构化存储
const input = (result as string).trim();
const [reviewResult, ...commentArr] = input.split(" ");
const reviewComment = commentArr.join(" ");
// 校验输入格式,避免错误
const validResults = ["approved", "rejected", "supplement"];
const finalResult = validResults.includes(reviewResult.toLowerCase())
? reviewResult.toLowerCase() as "approved" | "rejected" | "supplement"
: "supplement";
return {
manualReviewOpinion: {
reviewResult: finalResult,
reviewComment: reviewComment || "No comment provided",
}
};
}
// -------------------- 节点4:【HITL2】申请人补充材料 --------------------
// 原理:AI初审或人工复核要求补充材料时,触发这个节点,工作流暂停,等待申请人补充信息
function requestSupplement(state: typeof TradeApprovalState.State) {
const review = state.preliminaryReview;
const manualReview = state.manualReviewOpinion;
console.log("\n📋 HITL #2: APPLICANT SUPPLEMENT MATERIALS REQUIRED\n");
// 明确告诉申请人需要补充什么材料
let missingTips = "";
if (review?.missingMaterials && review.missingMaterials.length > 0) {
missingTips = review.missingMaterials.map((item, i) => `${i + 1}. ${item}`).join("\n");
}
if (manualReview?.reviewResult === "supplement") {
missingTips += `\nRisk Controller Comment: ${manualReview.reviewComment}`;
}
console.log("Please supplement the following materials/information:");
console.log(missingTips + "\n");
// 中断工作流,等待申请人补充材料
const result = interrupt({
question: "👤 Applicant, please enter your supplementary information: ",
});
return { supplementMaterial: result as string };
}
// -------------------- 节点5:生成最终审批结论 --------------------
// 原理:整合所有信息(交易申请、初审意见、人工复核、补充材料),生成最终审批结论,全链路审计留痕
async function generateFinalResult(state: typeof TradeApprovalState.State) {
console.log("\n📋 Generating final approval result...\n");
const trade = state.tradeApplication;
const review = state.preliminaryReview;
const manualReview = state.manualReviewOpinion;
const supplement = state.supplementMaterial || "";
// 全链路审计留痕,符合监管要求
const auditTrail: string[] = [
`[${new Date().toLocaleString()}] Trade application submitted by ${trade.payerName}`,
`[${new Date().toLocaleString()}] Preliminary review completed, risk level: ${review?.riskLevel}`,
];
if (manualReview) {
auditTrail.push(`[${new Date().toLocaleString()}] Manual review completed, result: ${manualReview.reviewResult}, comment: ${manualReview.reviewComment}`);
}
if (supplement) {
auditTrail.push(`[${new Date().toLocaleString()}] Applicant supplemented materials: ${supplement.substring(0, 100)}...`);
}
// 确定最终审批结果
let finalResult: "approved" | "rejected" = "approved";
if (manualReview?.reviewResult === "rejected") {
finalResult = "rejected";
} else if (!review?.needManualReview && review.riskLevel === "low") {
finalResult = "approved";
} else if (manualReview?.reviewResult === "approved") {
finalResult = "approved";
}
// 生成最终审批意见
const prompt = `
Generate a final professional trade approval opinion based on the following information:
TRADE APPLICATION:
Payer: ${trade.payerName} | Payee: ${trade.payeeName} (${trade.payeeType})
Amount: ¥${trade.tradeAmount} | Purpose: ${trade.tradePurpose}
Is Long-term Partner: ${trade.isLongTermPartner ? "Yes" : "No"}
PRELIMINARY REVIEW:
Risk Level: ${review?.riskLevel}
Opinion: ${review?.reviewOpinion}
MANUAL REVIEW OPINION (if any):
${manualReview ? `Result: ${manualReview.reviewResult}, Comment: ${manualReview.reviewComment}` : "No manual review, automatic approval"}
SUPPLEMENTARY MATERIALS (if any):
${supplement || "No supplementary materials"}
FINAL APPROVAL RESULT: ${finalResult.toUpperCase()}
REQUIREMENTS:
1. The opinion must be professional, rigorous, and in line with financial regulatory requirements.
2. Clearly state the approval basis and conclusion.
3. If rejected, clearly state the reason for rejection.
4. If approved, clearly state the follow-up requirements (if any).
`;
const response = await llm.invoke([
{
role: "system",
content: "You are a professional financial approval specialist, generate formal, rigorous final approval opinion.",
},
{ role: "user", content: prompt },
]);
const finalOpinion = response.content as string;
auditTrail.push(`[${new Date().toLocaleString()}] Final approval result generated: ${finalResult}`);
// 格式化打印最终结果
console.log("\n" + "=".repeat(100));
console.log(`🏦 FINAL TRADE APPROVAL RESULT: ${finalResult.toUpperCase()}`);
console.log("=".repeat(100) + "\n");
console.log(finalOpinion + "\n");
console.log("📋 AUDIT TRAIL (Regulatory Compliance):");
auditTrail.forEach((item, i) => {
console.log(`${i + 1}. ${item}`);
});
console.log("\n" + "=".repeat(100) + "\n");
return {
finalApprovalResult: {
result: finalResult,
finalOpinion,
auditTrail,
}
};
}
// ===================== 4. 构建审批工作流图【核心】 =====================
// 把所有节点串起来,定义先后顺序和条件分支,完整复现真实审批流程
const workflow = new StateGraph(TradeApprovalState)
// 第一步:添加所有流程节点
.addNode("searchApprovalBasis", searchApprovalBasis)
.addNode("generatePreliminaryReview", generatePreliminaryReview)
.addNode("manualReview", manualReview)
.addNode("requestSupplement", requestSupplement)
.addNode("generateFinalResult", generateFinalResult)
// 第二步:定义流程起点和基础顺序
.addEdge("__start__", "searchApprovalBasis") // 流程起点:先检索审批依据
.addEdge("searchApprovalBasis", "generatePreliminaryReview") // 检索完,生成初审意见
// 第三步:核心条件分支1:初审后,是自动审批、人工复核,还是要补充材料
.addConditionalEdges(
"generatePreliminaryReview",
(state: typeof TradeApprovalState.State) => {
const review = state.preliminaryReview;
if (!review) return "end";
// 优先级1:需要人工复核 → 走人工复核节点
if (review.needManualReview) return "manualReview";
// 优先级2:不需要人工复核,但需要补充材料 → 走补充材料节点
if (review.needSupplement) return "supplement";
// 优先级3:低风险,不需要人工和补充材料 → 直接生成最终结果(自动审批)
return "autoApprove";
},
{
manualReview: "manualReview", // 中高风险,人工复核
supplement: "requestSupplement", // 信息缺失,补充材料
autoApprove: "generateFinalResult", // 低风险,自动审批
}
)
// 第四步:核心条件分支2:人工复核后,是通过、驳回,还是要补充材料
.addConditionalEdges(
"manualReview",
(state: typeof TradeApprovalState.State) => {
const reviewResult = state.manualReviewOpinion?.reviewResult;
if (reviewResult === "rejected" || reviewResult === "approved") {
return "final"; // 通过/驳回,直接生成最终结果
}
return "supplement"; // 要求补充材料,走补充节点
},
{
final: "generateFinalResult",
supplement: "requestSupplement",
}
)
// 第五步:补充材料后,回到初审环节重审,最终生成结果
.addEdge("requestSupplement", "generatePreliminaryReview")
.addEdge("generateFinalResult", "__end__"); // 生成最终结果,流程结束
// ===================== 5. 主执行函数 =====================
async function main() {
// 第一步:先把审批数据导入ES
await ingestApprovalData();
// 【必加!】编译工作流,MemorySaver是存档器,没有它interrupt()无法生效,同时实现全流程留痕
const app = workflow.compile({ checkpointer: new MemorySaver() });
// 线程ID,用于区分不同的审批单,同一个ID可恢复审批进度,符合审计要求
const config = { configurable: { thread_id: "trade-approval-20240520-001" } };
// ===================== 在这里修改成你要测试的交易申请 =====================
const testTradeApplication = {
payerName: "XX Technology Co., Ltd.",
payeeName: "Li Si",
payeeType: "individual" as const,
tradeAmount: 850000, // 85万元,大额公转私,高风险,会触发人工复核
tradePurpose: "Project service fee",
contractNo: "",
isLongTermPartner: false,
payeeAddress: "Shenzhen, Guangdong",
};
// ==========================================================================
// 启动审批工作流,传入交易申请
let currentState = await app.invoke(
{ tradeApplication: testTradeApplication },
config
);
// 【HITL核心处理】循环处理所有中断,直到审批流程完全结束
while ((currentState as any).__interrupt__?.length > 0) {
console.log("\n💭 APPLICATION PAUSED. WAITING FOR USER INPUT...\n");
// 获取中断时的提示问题
const interruptQuestion = (currentState as any).__interrupt__[0]?.value?.question;
// 读取用户输入,不能为空
let userInput = "";
while (!userInput.trim()) {
userInput = await getUserInput(interruptQuestion || "👤 Please enter your response: ");
if (!userInput.trim()) {
console.log("⚠️ Input cannot be empty, please try again.\n");
}
}
// 把用户输入传给工作流,从断点继续运行
currentState = await app.invoke(
new Command({ resume: userInput.trim() }),
config
);
}
// 关闭终端输入
rl.close();
console.log("🎉 Trade Approval Workflow Completed! All records have been archived for audit.");
}
// 执行主函数
main().catch(console.error);
四、一键运行与全流程测试
1. 运行命令
所有文件创建完成后,在项目终端执行:
npx tsx main.ts
2. 测试流程演示(以85万大额公转私高风险交易为例)
运行后,你会看到完整的审批流程:
-
数据摄入:系统自动把审批案例、风控规则、监管政策导入ES 9.x
-
交易申请展示:打印你设置的测试交易信息
-
ES检索:匹配到对应的大额公转私风控规则、高风险审批案例、监管政策
-
AI初审:判定为高风险,强制要求人工复核,列出风险点
-
【HITL1 人工复核中断】:系统暂停,提示风控员输入复核意见
-
你可以输入:
rejected 大额公转私无合同支撑,无个税缴纳证明,直接驳回 -
也可以输入:
supplement 请补充服务合同、发票、个税申报凭证 -
也可以输入:
approved 已核实交易背景真实,材料齐全,同意通过
-
-
【HITL2 补充材料中断】:如果你选择了supplement,系统会再次暂停,提示申请人补充材料
-
最终审批结果:系统生成正式的审批结论,同时输出全链路审计留痕记录,符合监管要求
五、金融场景专属优化与合规提示
生产环境核心优化点
-
权限管控:给风控员、申请人设置不同的操作权限,HITL节点只能由对应角色操作
-
加密存储:交易信息、企业/个人信息必须加密存储,符合数据安全法要求
-
审计留痕:所有操作(包括人工输入的每一条内容)必须永久留存,不可篡改
-
规则引擎集成:把风控规则从ES里抽出来,做成独立的规则引擎,支持动态更新,无需重启系统
-
黑白名单实时校验:对接公安、工商、反洗钱黑名单系统,实时校验交易双方资质
-
对接核心系统:和企业ERP、银行核心系统对接,实现审批完成后自动转账
六、新手避坑指南
-
工作流不会暂停:检查
workflow.compile()里有没有加checkpointer: new MemorySaver(),没有存档器,中断功能无法生效 -
ES连不上报证书错误:检查
esClient里有没有加tls: { rejectUnauthorized: false },本地开发必须加 -
LLM输出格式错误:检查
withStructuredOutput的schema是否正确,temperature必须设为0,金融场景严禁自由输出 -
检索不到匹配的规则:检查数据摄入时有没有给每个文档生成向量,
bulk操作有没有加refresh: true -
监管留痕要求:生产环境必须把所有状态、人工输入、审批结果持久化到数据库,不能只用MemorySaver(内存存储,重启就丢)
更多推荐

所有评论(0)