图片来源网络,侵权联系删。

文章目录
- [1. 从"手动刷票"到"AI代劳",Function Calling如何重构出行体验?](#1. 从“手动刷票”到“AI代劳”,Function Calling如何重构出行体验?)
- [2. 为什么Function Calling适合复杂业务流程自动化?](#2. 为什么Function Calling适合复杂业务流程自动化?)
- [3. 将订票流程拆解为受控工具链](#3. 将订票流程拆解为受控工具链)
- [4. 构建12306自动订票Agent](#4. 构建12306自动订票Agent)
-
- [4.1 后端:Spring Boot封装12306工具函数](#4.1 后端:Spring Boot封装12306工具函数)
- 步骤1:建立全局会话管理(模拟浏览器Cookie)
- 步骤2:实现核心工具函数
- 步骤3:注册工具函数供Agent调用
- [4.2 前端:React处理多步交互与验证码](#4.2 前端:React处理多步交互与验证码)
- [5. 合规性与反爬策略](#5. 合规性与反爬策略)
-
- [5.1 遵守12306使用条款](#5.1 遵守12306使用条款)
- [5.2 应对反爬机制](#5.2 应对反爬机制)
- [6. 总结与Web开发者的AI工程化建议](#6. 总结与Web开发者的AI工程化建议)
-
- [6.1 进阶方向:](#6.1 进阶方向:)
- [6.2 重要提醒:](#6.2 重要提醒:)
1. 从"手动刷票"到"AI代劳",Function Calling如何重构出行体验?
每逢节假日,抢购12306火车票是无数用户的痛点:反复刷新、验证码干扰、余票瞬变......传统做法是用户自己操作浏览器或使用第三方脚本(存在封号风险)。
而借助 LLM + Function Calling ,我们可以构建一个合规、安全、可解释的自动订票Agent:
- 用户用自然语言描述需求:"帮我订下周三北京到上海的高铁,二等座"
- Agent解析意图,调用封装好的12306工具函数
- 自动完成余票查询 → 验证码识别 → 提交订单全流程
- 前端实时反馈进度(如"正在排队..."、"验证码已识别")
这不是"外挂",而是将你已有的Web自动化能力(如Selenium、HTTP客户端)封装为AI可调用的工具函数,由模型智能调度。
本文将基于Java(Spring Boot)+ React技术栈,实战构建一个支持多步交互、验证码处理、会话保持的12306订票Agent,完全贴合Web开发者工程习惯。

2. 为什么Function Calling适合复杂业务流程自动化?
12306订票涉及多步骤、状态依赖、反爬机制,直接让模型输出完整代码不可行。但Function Calling提供了"分步可控"的解决方案:
| 挑战 | Web开发类比 | Function Calling应对策略 |
|---|---|---|
| 多步骤依赖(查票→选座→支付) | 多页面表单提交 | 每个步骤封装为独立工具函数 |
| 验证码动态变化 | 图形验证码校验 | 工具函数内嵌OCR/人工识别回调 |
| 会话保持(Cookie/JSESSIONID) | 用户登录态管理 | 全局会话上下文自动注入 |
| 反爬限制(IP/频率) | API限流熔断 | 工具函数内置重试与代理轮换 |
核心原则:模型只负责"决策下一步做什么",你掌控"每一步如何安全执行"。这与Web后端编排微服务的思路完全一致。

3. 将订票流程拆解为受控工具链
我们定义一组工具函数,覆盖订票全链路:
json
[
{
"name": "search_trains",
"description": "查询指定日期、起终点的余票信息",
"parameters": { "from": "string", "to": "string", "date": "string (YYYY-MM-DD)" }
},
{
"name": "submit_order",
"description": "提交车票订单(需先查票)",
"parameters": { "train_no": "string", "seat_type": "string" }
},
{
"name": "handle_captcha",
"description": "处理12306图形验证码",
"parameters": { "image_url": "string" }
}
]
多步交互流程:
12306 submit_order() handle_captcha() search_trains() Agent User 12306 submit_order() handle_captcha() search_trains() Agent User "订6月15日北京到上海的G1次二等座" 调用 {from:"北京", to:"上海", date:"2026-06-15"} 返回余票列表(含G1有票) 调用 {train_no:"G1", seat_type:"二等座"} 触发验证码,调用 handle_captcha(image_url) 返回识别结果 "click: [x1,y1], [x2,y2]" 提交带验证码的订单 订单成功! "已成功预订G1次列车,订单号:E123456"
注意:
handle_captcha可能需要人工介入(前端弹出图片让用户点击),体现"人在环路"设计。

4. 构建12306自动订票Agent
4.1 后端:Spring Boot封装12306工具函数
步骤1:建立全局会话管理(模拟浏览器Cookie)
java
// TicketSessionManager.java
@Component
public class TicketSessionManager {
private final Map<String, CookieStore> userSessions = new ConcurrentHashMap<>();
public CookieStore getSession(String userId) {
return userSessions.computeIfAbsent(userId, k -> new BasicCookieStore());
}
// 在每次HTTP请求中自动附加Cookie
public CloseableHttpClient createClient(String userId) {
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(10000)
.build();
return HttpClients.custom()
.setDefaultCookieStore(getSession(userId))
.setDefaultRequestConfig(config)
.build();
}
}
步骤2:实现核心工具函数
java
// TrainBookingService.java
@Service
@RequiredArgsConstructor
public class TrainBookingService {
private final TicketSessionManager sessionManager;
private final CaptchaService captchaService; // 验证码识别服务
// 工具函数1:查询余票
public List<TrainInfo> searchTrains(String from, String to, String date, String userId) {
try (CloseableHttpClient client = sessionManager.createClient(userId)) {
// 1. 获取12306查询参数(需先请求JS加密逻辑,此处简化)
String url = String.format("https://kyfw.12306.cn/otn/leftTicket/query?leftTicketDTO.train_date=%s&leftTicketDTO.from_station=%s&leftTicketDTO.to_station=%s",
date, getStationCode(from), getStationCode(to));
HttpResponse response = client.execute(new HttpGet(url));
String json = EntityUtils.toString(response.getEntity());
// 2. 解析余票数据(省略具体字段映射)
return parseTrainList(json);
} catch (Exception e) {
throw new RuntimeException("查询失败", e);
}
}
// 工具函数2:提交订单(含验证码处理)
public String submitOrder(String trainNo, String seatType, String userId) {
try (CloseableHttpClient client = sessionManager.createClient(userId)) {
// 1. 发起预提交,可能触发验证码
HttpPost preSubmit = new HttpPost("https://kyfw.12306.cn/otn/confirmPassenger/checkOrderInfo");
// ... 设置请求体(车次、座位等)
HttpResponse resp = client.execute(preSubmit);
String result = EntityUtils.toString(resp.getEntity());
if (needsCaptcha(result)) {
// 2. 获取验证码图片
String captchaUrl = "https://kyfw.12306.cn/otn/passcodeNew/getPassCodeNew?module=passenger&rand=randp";
byte[] imageBytes = downloadImage(client, captchaUrl);
// 3. 调用验证码处理工具(可能返回人工识别任务ID)
CaptchaResult captcha = captchaService.process(imageBytes, userId);
if (captcha.isManualRequired()) {
// 抛出特殊异常,前端捕获后弹出验证码图片
throw new ManualCaptchaRequiredException(captcha.getTaskId(), imageBytes);
}
// 4. 提交带验证码的答案
HttpPost confirm = new HttpPost("https://kyfw.12306.cn/otn/confirmPassenger/resultOrderForDcQueue");
// ... 附加验证码坐标
HttpResponse finalResp = client.execute(confirm);
return parseOrderId(EntityUtils.toString(finalResp.getEntity()));
}
return parseOrderId(result);
} catch (Exception e) {
throw new RuntimeException("订票失败", e);
}
}
}
步骤3:注册工具函数供Agent调用
java
// FunctionRegistry.java
public List<ChatCompletionTool> getBookingTools() {
return Arrays.asList(
ChatCompletionTool.builder()
.function(FunctionDefinition.builder()
.name("search_trains")
.description("查询火车余票,参数:出发地、目的地、日期(YYYY-MM-DD)")
.parameters(createObjectSchema(
field("from", "出发城市,如'北京'"),
field("to", "目的地城市,如'上海'"),
field("date", "乘车日期,格式YYYY-MM-DD")
))
.build())
.build(),
ChatCompletionTool.builder()
.function(FunctionDefinition.builder()
.name("submit_order")
.description("提交订单,需先查询余票")
.parameters(createObjectSchema(
field("train_no", "车次,如'G1'"),
field("seat_type", "座位类型,如'二等座'")
))
.build())
.build()
);
}
4.2 前端:React处理多步交互与验证码
jsx
// BookingAgent.jsx
import { useReducer, useState } from 'react';
const initialState = { messages: [], status: 'idle', captchaTask: null };
function bookingReducer(state, action) {
switch (action.type) {
case 'ADD_MESSAGE':
return { ...state, messages: [...state.messages, action.payload] };
case 'SET_STATUS':
return { ...state, status: action.payload };
case 'REQUIRE_CAPTCHA':
return { ...state, captchaTask: action.payload, status: 'awaiting_captcha' };
case 'CAPTCHA_SUBMITTED':
return { ...state, captchaTask: null, status: 'processing' };
default:
return state;
}
}
export default function BookingAgent() {
const [state, dispatch] = useReducer(bookingReducer, initialState);
const [captchaClicks, setCaptchaClicks] = useState([]);
const handleBook = async (query) => {
dispatch({ type: 'ADD_MESSAGE', payload: { role: 'user', content: query } });
dispatch({ type: 'SET_STATUS', payload: 'processing' });
try {
const res = await fetch('/api/agent/book', {
method: 'POST',
body: JSON.stringify({ query, userId: 'user123' })
});
const data = await res.json();
if (data.requiresCaptcha) {
// 显示验证码图片,等待用户点击
dispatch({
type: 'REQUIRE_CAPTCHA',
payload: { image: data.captchaImage, taskId: data.taskId }
});
} else {
dispatch({ type: 'ADD_MESSAGE', payload: { role: 'agent', content: data.result } });
dispatch({ type: 'SET_STATUS', payload: 'idle' });
}
} catch (error) {
dispatch({ type: 'ADD_MESSAGE', payload: { role: 'agent', content: `错误: ${error.message}` } });
dispatch({ type: 'SET_STATUS', payload: 'idle' });
}
};
const handleCaptchaSubmit = async () => {
dispatch({ type: 'CAPTCHA_SUBMITTED' });
// 提交用户点击的坐标
await fetch('/api/captcha/submit', {
method: 'POST',
body: JSON.stringify({
taskId: state.captchaTask.taskId,
clicks: captchaClicks
})
});
// 继续订票流程...
// (实际中需轮询结果或WebSocket通知)
};
return (
<div>
{/* 聊天记录 */}
{state.messages.map((msg, i) => (
<div key={i}>{msg.role === 'user' ? `👤 ${msg.content}` : `🤖 ${msg.content}`}</div>
))}
{/* 验证码处理 */}
{state.captchaTask && (
<div className="captcha-container">
<img
src={`data:image/png;base64,${state.captchaTask.image}`}
onClick={(e) => {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setCaptchaClicks([...captchaClicks, { x, y }]);
}}
alt="请点击验证码中的文字"
/>
<button onClick={handleCaptchaSubmit}>提交验证码</button>
</div>
)}
<input
type="text"
placeholder="例如:订6月15日北京到上海的G1次二等座"
onKeyDown={(e) => e.key === 'Enter' && handleBook(e.target.value)}
/>
</div>
);
}
前端在遇到验证码时暂停流程,将控制权交还给用户,既合规又提升成功率。

5. 合规性与反爬策略
5.1 遵守12306使用条款
- 不用于商业牟利:仅限个人辅助
- 低频请求:工具函数内置延迟(如查票间隔≥2秒)
- 模拟真实用户:设置合法User-Agent、Accept-Language
5.2 应对反爬机制
- IP轮换:通过代理池切换出口IP(工具函数内集成)
- 验证码兜底:当OCR失败时,自动转人工识别(前端交互)
- 会话保鲜:定期刷新Cookie(如每5分钟访问首页)
java
// 在工具函数中加入请求间隔
private void enforceRateLimit(String userId) {
Long lastRequest = lastRequestTime.get(userId);
if (lastRequest != null && System.currentTimeMillis() - lastRequest < 2000) {
try {
Thread.sleep(2000 - (System.currentTimeMillis() - lastRequest));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
lastRequestTime.put(userId, System.currentTimeMillis());
}

6. 总结与Web开发者的AI工程化建议
通过Function Calling构建12306订票Agent,不是为了绕过规则,而是将繁琐操作自动化,同时保留关键人工干预点(如验证码)。你作为Web开发者的核心价值在于:
- 封装复杂性:将12306的HTTP协议细节隐藏在工具函数内
- 保障可靠性:通过会话管理、重试机制、速率控制提升成功率
- 设计人机协作:在AI无法处理的环节(验证码)优雅交还控制权
6.1 进阶方向:
- 多账号支持:为家庭成员并行订票(需独立会话)
- 候补监控:当无票时自动进入候补,并通知用户
- 语音交互:集成语音识别,实现"动口不动手"订票
6.2 重要提醒:
- 本文仅作技术探讨,请勿用于高频刷票或商业用途
- 12306接口可能随时变更,请做好容错设计
- 推荐优先使用官方App或Web端,自动化仅作辅助
真正的智能,是在效率与合规之间找到平衡点。而你,正是这个平衡的设计师。
