第三章 04:3.6 基础调用链的构建
课程专题 :《AI Agents 开发实践》
原文链接 :
https://s.juejin.cn/ds/TAqu_hbSmd8/章节 :第三章 3.6 基础调用链的构建
作者:Java 开发者转型 AI 全栈
3.6.1 pipe() 的链式组合能力
问题
现在调用模型要写三步:
typescript
const messages = await requirementPrompt.formatMessages({ input }); // 1. 渲染模板
const response = await this.model.invoke(messages); // 2. 调模型
return { result: response.content }; // 3. 取内容
如果这组步骤在多个地方重复出现,就该把它们串成一条链,以后只调链,不重复写拼装逻辑。
Java 对照
java
// 反面:每次调用都手动拼装
public String process(String input) {
String prompt = template.render(input); // 1. 渲染
String result = model.call(prompt); // 2. 调用
return parser.parse(result); // 3. 解析
}
// 正面:用 Pipeline/Chain 模式封装
@Component
public class RequirementChain {
private final Pipeline<String, String> pipeline = Pipeline.of(
template::render, // 1. 渲染
model::call, // 2. 调用
parser::parse // 3. 解析
);
public String process(String input) {
return pipeline.execute(input);
}
}
3.6.2 StringOutputParser 的接入
最小调用链通常由三段组成:提示模板、模型、输出解析。这里先接入一个最基础的 StringOutputParser,把模型输出统一转成纯字符串。
services/chat/src/llm/requirement.chain.ts
typescript
// services/chat/src/llm/requirement.chain.ts
// 最小调用链:提示模板 → 模型 → 输出解析,pipe() 串成固定流程
import { StringOutputParser } from '@langchain/core/output_parsers';
import { createChatModel } from './model.factory';
import { requirementPrompt } from './requirement.prompt-builder';
const model = createChatModel();
const parser = new StringOutputParser();
export const requirementChain = requirementPrompt.pipe(model).pipe(parser);
3.6.3 chain.invoke() 的调用方式
链建好之后,最先用到的仍然是 invoke()。只不过这时你调用的,已经不再是"一个模型",而是一条固定流程。
Service 新增方法
typescript
// 在 llm.service.ts 中新增
import { requirementChain } from './requirement.chain';
async chainInvoke(input: string) {
const result = await requirementChain.invoke({ input });
return { result };
}
Controller 新增路由
typescript
// 在 llm.controller.ts 中新增
@Post('chain-invoke')
async chainInvoke(@Body() body: { input: string }) {
return this.llmService.chainInvoke(body.input);
}
验收测试
cmd
curl.exe -s -X POST http://localhost:4001/api/langchain/chain-invoke -H "Content-Type: application/json" -d "{\"input\":\"用户注册时必须绑定手机号,密码至少 8 位\"}"
期望返回:
json
{
"result": "{\n \"action\": \"绑定手机号\",\n \"constraints\": [\n \"必须\",\n \"至少 8 位\"\n ],\n \"entities\": [\n \"用户\",\n \"手机号\",\n \"密码\"\n ]\n}"
}
重点看 :result 是纯字符串(StringOutputParser 生效),不再有 3.4 里 response.content 那种 MessageContent 包装。
3.6.4 chain.stream() 的流式执行
如果同一条固定流程需要边执行边返回,那么也没必要退回手写拼接。链本身同样支持 stream()。
Service 新增方法
typescript
async chainStream(input: string) {
return requirementChain.stream({ input });
}
Controller 新增路由(SSE 方式)
typescript
@Post('chain-stream')
async chainStream(@Body() body: { input: string }, @Res() res: Response) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await this.llmService.chainStream(body.input);
for await (const chunk of stream) {
res.write(chunk);
}
res.end();
}
注意 :parser 之后 chunk 已是纯字符串,直接 res.write(chunk) 即可。
3.6.5 chain.batch() 的批量执行
同理,如果这条标准流程需要在多条输入上重复执行,那么也可以直接切到 batch()。
Service 新增方法
typescript
async chainBatch(inputs: string[]) {
const results = await requirementChain.batch(
inputs.map((input) => ({ input })),
);
return results.map((result, i) => ({ index: i + 1, result }));
}
Controller 新增路由
typescript
@Post('chain-batch')
async chainBatch(@Body() body: { inputs: string[] }) {
return this.llmService.chainBatch(body.inputs);
}
验收测试
cmd
curl.exe -s -X POST http://localhost:4001/api/langchain/chain-batch -H "Content-Type: application/json" -d "{\"inputs\":[\"用户注册时必须绑定手机号,密码至少 8 位\",\"订单支付后 7 天内可申请退款\"]}"
期望返回:
json
[
{
"index": 1,
"result": "{\n \"action\": \"绑定手机号\",\n \"constraints\": [\n \"必须\",\n \"至少 8 位\"\n ],\n \"entities\": [\n \"用户\",\n \"手机号\",\n \"密码\"\n ]\n}"
},
{
"index": 2,
"result": "{\n \"action\": \"申请退款\",\n \"constraints\": [\n \"7 天内\"\n ],\n \"entities\": [\n \"订单\",\n \"支付\",\n \"退款\"\n ]\n}"
}
]
3.6.6 链式调用的具体使用场景
到这一节,重点已经不再是单独使用 prompt、model 或 parser,而是:当一组步骤总是以同样顺序反复出现时,就应该把它们收束成一条可以重复执行的链。
和 3.4 里直接调用模型相比,3.6 更适合解决这类问题:
- 你的输入前面总要先走一次提示模板填充
- 你的输出后面总要接一次统一解析
- 同一套流程会在多个接口、多个任务里反复复用
- 你希望把"提示 + 模型 + 解析"当成一个整体来调用、测试和替换
chain.invoke():适合固定流程的一次性结果返回
当你的场景不是"裸调模型",而是"模板 → 模型 → 解析"这一整套流程都必须稳定执行时,chain.invoke() 会成为最自然的默认选择。
典型场景:
- 需求抽取接口:
POST /requirement/extract - 文案标准化改写
- 分类、打标、轻量判断类任务
- 服务层复用(多个 Controller、定时任务、后台脚本)
chain.stream():适合同一条流程需要边产出边展示
如果这条链本身已经稳定,但输出内容较长、用户等待时间较明显,或者前端希望边生成边展示,那么就适合把同一条链切到 stream()。
典型场景:
- 长文本生成(分析说明、接口草案、测试建议)
- 前端即时反馈(聊天页、AI 辅助写作页)
- 后台实时透出过程结果(日志面板、控制台、SSE 通道)
工程思路 :先把固定流程写成 chain,之后再根据产品需要选择 invoke() 还是 stream()。这样"业务流程"和"返回方式"是分开的。
chain.batch():适合批量跑同一条标准处理链
当同一个处理链要在多条输入上重复执行时,chain.batch() 会更合适。它解决的不是"能不能跑",而是"怎样更成批、更统一地跑"。
典型场景:
- 历史数据回填(批量抽取旧需求文档)
- 导入前预处理(批量标准化整理、分类或摘要提取)
- 评测与回归测试(拿一组样本批量重跑,对比前后输出差异)
- 定时任务 / 离线任务(晚上低峰时批量处理待分析文本)
什么时候优先上链,而不是继续手写分散调用?
可以用一个很直接的判断标准:
如果你已经连续两次以上写出"同一个 prompt + 同一个 model + 同一个 parser"的组合,就应该考虑把它收成 chain。
因为从这个时点开始,你真正要维护的,已经不再是某次调用,而是一条业务流程。
判断标准
什么时候该上链?
当"模板 → 模型 → 解析"这组步骤已经稳定,并且在多个接口或任务中被反复调用时。
下一章
3.7 结构化输出与程序化消费:让模型直接返回可被代码消费的 JSON 对象,而不是"长得像 JSON 的字符串"。
敬请期待!
回到索引 :第三章博客索引