第三章 04:3.6 基础调用链的构建

第三章 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 的字符串"。

敬请期待!


回到索引 :第三章博客索引

相关推荐
青少儿编程课堂1 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:拓扑排序课程表排课解析
c++·python·算法·bfs·信息学竞赛
向上的车轮1 小时前
Python 大数据处理不慢:Polars + DuckDB + Arrow 实战指南
开发语言·python
Nebula_g1 小时前
云禾外卖补充—项目整体后端运行逻辑-对流程的更深层次理解
java·开发语言·网络·数据库·状态模式·javase
程序员小孔.1 小时前
基于 SpringBoot + UniApp + Vue 的家政服务系统设计与实现
前端·数据库·spring boot·python·docker·微信小程序
程序猿乐锅2 小时前
【从0-1学习Python | 第六篇】 Python 函数:定义、参数与返回值
开发语言·python·学习
G018_star sky♬2 小时前
java中的字符串常见方法
java·开发语言
熊猫钓鱼>_>2 小时前
Kotlin Multiplatform for OpenHarmony 实战:kotlinx-serialization JSON 适配
开发语言·华为·kotlin·华为云·json·harmonyos·code
MasonYyp3 小时前
声音转文字的模型
python
银河技术3 小时前
从脚本到 Operator:用 Go 构建可靠的微服务自动扩缩容控制器
开发语言·微服务·golang