Web开发者快速上手AI Agent:基于Function Calling的12306自动订票系统实战

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

文章目录

  • [1. 从"手动刷票"到"AI代劳",Function Calling如何重构出行体验?](#1. 从“手动刷票”到“AI代劳”,Function Calling如何重构出行体验?)
  • [2. 为什么Function Calling适合复杂业务流程自动化?](#2. 为什么Function Calling适合复杂业务流程自动化?)
  • [3. 将订票流程拆解为受控工具链](#3. 将订票流程拆解为受控工具链)
  • [4. 构建12306自动订票Agent](#4. 构建12306自动订票Agent)
  • [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 进阶方向:

  1. 多账号支持:为家庭成员并行订票(需独立会话)
  2. 候补监控:当无票时自动进入候补,并通知用户
  3. 语音交互:集成语音识别,实现"动口不动手"订票

6.2 重要提醒:

  • 本文仅作技术探讨,请勿用于高频刷票或商业用途
  • 12306接口可能随时变更,请做好容错设计
  • 推荐优先使用官方App或Web端,自动化仅作辅助

真正的智能,是在效率与合规之间找到平衡点。而你,正是这个平衡的设计师。

相关推荐
CRUD酱1 天前
后端使用POI解析.xlsx文件(附源码)
java·后端
EchoL、1 天前
浅谈当下深度生成模型:从VAE、GAN、Diffusion、Flow Matching到世界模型
人工智能·神经网络·生成对抗网络
亓才孓1 天前
多态:编译时看左边,运行时看右边
java·开发语言
凤希AI伴侣1 天前
深度优化与开源力量-凤希AI伴侣-2026年1月6日
人工智能·凤希ai伴侣
deephub1 天前
Agentic RAG:用LangGraph打造会自动修正检索错误的 RAG 系统
人工智能·大语言模型·rag·langgraph
Rabbit_QL1 天前
【Pytorch使用】CUDA 显存管理与 OOM 排查实战:以 PyTorch 联邦学习训练为例
人工智能·pytorch·python
坠金1 天前
方差、偏差
人工智能·机器学习
2501_941802481 天前
从缓存更新到数据一致性的互联网工程语法实践与多语言探索
java·后端·spring
强盛小灵通专卖员1 天前
airsim无人机仿真深度强化学习自动避障辅导
人工智能·无人机·sci·深度强化学习·airsim·自动避障·小论文