城智连响:基于 LangGraph 与四库分层架构的城市公共设施智能报修与派单系统

城智连响:基于 LangGraph 与四库分层架构的城市公共设施智能报修与派单系统

一个面向「市民报修 → AI 解析 → 智能派单 → 到场维修 → AI 验收 → 结算闭环」全链路的人工智能应用开发实战项目。

技术栈:FastAPI + SQLAlchemy(异步) + MySQL / Redis / MongoDB / Elasticsearch / RabbitMQ + LangChain + LangGraph + 阿里云百炼(qwen-vl-max) + 高德地图 API。


目录

  1. 项目背景与痛点
  2. [总体架构:四库分层 + 消息驱动](#总体架构:四库分层 + 消息驱动)
  3. [AI 决策链:LangGraph 事件驱动短图](#AI 决策链:LangGraph 事件驱动短图)
  4. [三大 AI 决策点详解](#三大 AI 决策点详解)
  5. [派单决策:硬约束 + 四维加权 + LLM 工具循环](#派单决策:硬约束 + 四维加权 + LLM 工具循环)
  6. 响应速度指标:增量平均算法
  7. 可靠性工程:锁、延迟队列与降级
  8. 数据存储设计
  9. 关键代码走读
  10. 工程实践与踩坑总结
  11. 总结与展望

1. 项目背景与痛点

传统城市公共设施的报修高度依赖人工:市民电话报修、话务员手工记录、调度员凭经验派单、维修员到场靠口头反馈。这种模式存在三个典型问题:

  • 信息割裂:市民(报修方)、维修员(服务方)、管理员(调度方)三方各自维护一套信息,报修内容靠人工转述,容易失真、遗漏。
  • 派单依赖经验:调度员「就近派单」凭直觉,缺乏对维修员实时负载、技能、好评率、响应速度的综合量化,导致「忙的忙死、闲的闲死」。
  • 验收靠人工核验:维修是否合格依赖管理员事后抽查,缺乏客观、可复用的验收标准。

本系统以「城市公共设施智能报修与派单」为场景,将 AI 能力嵌入到三个关键决策点------报修解析、派单决策、视觉验收,并围绕「最小运维总成本」目标构建了一套可落地、可降级的智能派单链路。


2. 总体架构:四库分层 + 消息驱动

系统采用 「四库分层存储 + 消息队列解耦」 的架构。核心思想是:MySQL 是唯一同步阻断点(事务性事实源),其余写入全部异步化、可容错

txt 复制代码
                 ┌─────────────────────────────────────────────┐
                 │                FastAPI 应用层                 │
                 │  /api/v1/citizen · /api/v1/worker · /api/v1/admin │
                 └───────┬───────────────────────────┬──────────┘
                         │                           │
          ┌──────────────▼──────────────┐   ┌────────▼─────────────────┐
          │        LangGraph 决策链       │   │    RabbitMQ 消费者(后台)  │
          │  route → parse/dispatch/verify│   │  dispatch / timeout /     │
          │        → rework (短图)        │   │  es_sync(重试+DLQ)        │
          └───────┬──────────────────────┘   └────────┬─────────────────┘
                  │                                     │
   ┌──────────────┼──────────────┬───────────────┬──────┴──────────────┐
   ▼              ▼              ▼               ▼                      ▼
 MySQL        Redis          MongoDB        Elasticsearch          RabbitMQ
 事务事实源    缓存/Geo/锁/计数  文档/日志        搜索索引              消息总线

各存储的职责边界(这是本项目最重要的设计取舍之一):

存储 定位 承载数据
MySQL 事务性事实源(唯一同步阻断点) tickets 工单主表、workers 维修员档案、settlements 结算单、audit_rules 结算规则
Redis 高并发读 + 原子操作(拆 4 个逻辑库) DB0 热状态缓存 ticket:{id}:info、DB1 Geo workers:geo、DB2 分布式锁 lock:ticket:{id}、DB3 计数器 worker:{id}:daily_order
MongoDB 灵活文档 + 过程日志 ai_analysis_logs AI 分析日志、repair_records 维修记录、ticket_attachments 图片元数据、notifications 通知、audit_logs 审计
Elasticsearch 全文检索 + 绩效聚合 tickets_index 工单检索、workers_perf_index 维修员绩效
RabbitMQ 异步解耦 + 延迟调度 dispatchdispatch_timeout(延迟队列)、es_sync(重试+DLQ)、review_queue

设计原则:MySQL 写是唯一同步阻断点,其余操作(NLP 解析、Mongo 日志、ES 索引、消息发布)全部异步 + try/except 容错------单个环节失败绝不影响主流程。这一原则贯穿了从报修到结算的每一处代码。


3. AI 决策链:LangGraph 事件驱动短图

3.1 为什么是「事件驱动短图」而非「长驻状态机」

这是本项目最核心的架构决策。项目初期曾考虑用 LangGraph 的 checkpointer 持久化图状态、让一张长生命周期状态机「驻留」在内存中跟踪每个工单的全流程。最终否决,理由如下:

  1. 工单生命周期本质是「天级」的:从报修到结算可能跨越数小时到数天,把状态驻留在进程内存既不现实也脆弱(重启即丢)。
  2. 状态已有事实源 :工单状态流转已经由 MySQL tickets.status 承担,且 Redis 里有热缓存、MongoDB 里有过程日志,没必要再引入第三份「图状态」。
  3. 业务事件天然离散:报修(report)、超时(timeout)、完工(complete)是三个独立的触发点,每个事件只需要「跑几秒钟、做完决策、退出」。

因此落地为 事件驱动短图 :每个业务事件触发一次 ainvoke(event=...),图在秒级内跑完即退出;工单生命周期状态存 MySQL,RabbitMQ 延迟队列负责「唤醒」下一次图执行。

python 复制代码
# agent_state.py ------ 状态仅在单次图执行内流转,不持久化
class TicketState(TypedDict, total=False):
    event: str                    # "report" | "timeout" | "complete"
    ticket_id: str
    description: str
    image_urls: List[str]
    ai_category: str
    ai_confidence: float
    emergency_level: int
    selected_worker_id: Optional[str]
    assigned_worker_id: Optional[str]
    dispatch_result: dict[str, Any]
    verdict: Optional[bool]
    verify_confidence: float
    diff_summary: str
    before_photo_urls: List[str]
    after_photo_urls: List[str]
    repair_description: str

DB 会话不进 state ,而是通过 ainvoke(config={"configurable": {"db": session}}) 传入,节点从 config["configurable"]["db"] 取。这样避免了把不可序列化的 AsyncSession 塞进图状态,也天然隔离了每次事件执行的数据库事务边界。

3.2 图结构:五节点 + 条件边 + 无 checkpointer

python 复制代码
builder = StateGraph(TicketState)
builder.add_node("route", route_node)      # 薄路由:加载工单、映射字段
builder.add_node("parse", parse_node)      # 报修解析决策点
builder.add_node("dispatch", dispatch_node) # 派单决策点
builder.add_node("verify", verify_node)    # 视觉验收决策点
builder.add_node("rework", rework_node)    # 返工副作用节点

builder.add_edge(START, "route")
builder.add_conditional_edges("route", _route_by_event,
    {"parse": "parse", "dispatch": "dispatch", "verify": "verify"})
builder.add_edge("parse", END)
builder.add_edge("dispatch", END)
builder.add_conditional_edges("verify", _route_by_verdict,
    {"rework": "rework", "end": END})
builder.add_edge("rework", END)

decision_graph = builder.compile()  # 不传 checkpointer,不持久化图状态

#mermaid-svg-YyJcDqMT5Qr0HzpU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YyJcDqMT5Qr0HzpU .error-icon{fill:#552222;}#mermaid-svg-YyJcDqMT5Qr0HzpU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YyJcDqMT5Qr0HzpU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .marker.cross{stroke:#333333;}#mermaid-svg-YyJcDqMT5Qr0HzpU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YyJcDqMT5Qr0HzpU p{margin:0;}#mermaid-svg-YyJcDqMT5Qr0HzpU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .cluster-label text{fill:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .cluster-label span{color:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .cluster-label span p{background-color:transparent;}#mermaid-svg-YyJcDqMT5Qr0HzpU .label text,#mermaid-svg-YyJcDqMT5Qr0HzpU span{fill:#333;color:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .node rect,#mermaid-svg-YyJcDqMT5Qr0HzpU .node circle,#mermaid-svg-YyJcDqMT5Qr0HzpU .node ellipse,#mermaid-svg-YyJcDqMT5Qr0HzpU .node polygon,#mermaid-svg-YyJcDqMT5Qr0HzpU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .rough-node .label text,#mermaid-svg-YyJcDqMT5Qr0HzpU .node .label text,#mermaid-svg-YyJcDqMT5Qr0HzpU .image-shape .label,#mermaid-svg-YyJcDqMT5Qr0HzpU .icon-shape .label{text-anchor:middle;}#mermaid-svg-YyJcDqMT5Qr0HzpU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .rough-node .label,#mermaid-svg-YyJcDqMT5Qr0HzpU .node .label,#mermaid-svg-YyJcDqMT5Qr0HzpU .image-shape .label,#mermaid-svg-YyJcDqMT5Qr0HzpU .icon-shape .label{text-align:center;}#mermaid-svg-YyJcDqMT5Qr0HzpU .node.clickable{cursor:pointer;}#mermaid-svg-YyJcDqMT5Qr0HzpU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .arrowheadPath{fill:#333333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YyJcDqMT5Qr0HzpU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YyJcDqMT5Qr0HzpU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YyJcDqMT5Qr0HzpU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YyJcDqMT5Qr0HzpU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .cluster text{fill:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU .cluster span{color:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YyJcDqMT5Qr0HzpU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YyJcDqMT5Qr0HzpU rect.text{fill:none;stroke-width:0;}#mermaid-svg-YyJcDqMT5Qr0HzpU .icon-shape,#mermaid-svg-YyJcDqMT5Qr0HzpU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YyJcDqMT5Qr0HzpU .icon-shape p,#mermaid-svg-YyJcDqMT5Qr0HzpU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YyJcDqMT5Qr0HzpU .icon-shape .label rect,#mermaid-svg-YyJcDqMT5Qr0HzpU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YyJcDqMT5Qr0HzpU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YyJcDqMT5Qr0HzpU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YyJcDqMT5Qr0HzpU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} event=report
event=timeout
event=complete
verdict=True
verdict=False
START
route 入口路由
parse 报修解析
dispatch 派单决策
verify 视觉验收
END
rework 返工

节点边界规则 (这也是一个值得记下的设计约定):LangGraph 节点 = AI 决策点;工具调用、硬约束、评分算法、降级回退都是「节点内部」的实现细节,不提升为独立节点。这样做让图保持扁平,避免了把确定性业务逻辑(如硬约束过滤)错误地抽象成图节点而过度设计。

3.3 三个调用点如何「唤醒」图

业务事件 触发位置 图入口 节点链路
市民报修 report_service.submit_repair_report event=report route → parse
10 分钟超时 main.handle_timeout(RabbitMQ 消费者) event=timeout route → dispatch
维修员完工 repair_service.worker_complete event=complete route → verify(→ rework)

以报修为例,report_service 在同步落地 MySQL 之后,把原本内联的 NLP 解析段落替换为一次图调用,失败仍然只降级为默认分类、绝不阻塞主流程:

python 复制代码
ai_category = "其他设施"
try:
    await decision_graph.ainvoke(
        {"event": "report", "ticket_id": ticket_id,
         "description": description, "image_urls": image_urls},
        config={"configurable": {"db": db}},
    )
    fresh = (await db.execute(
        select(Ticket).where(Ticket.ticket_id == ticket_id)
    )).scalar_one_or_none()
    ai_category = fresh.ai_category if fresh and fresh.ai_category else "其他设施"
except Exception as e:
    logger.warning(f"NLP解析失败(使用默认值): {e}")

4. 三大 AI 决策点详解

4.1 parse 节点:报修解析

输入市民文字描述(可选图片 + 坐标),输出归一化的 {category, sub_category, emergency_level, confidence, ...} 结构化结果,回写 MySQL 的 ai_category / ai_confidence / emergency_level,并落 MongoDB ai_analysis_logs

实现上走 LangChain with_structured_output ------用 Pydantic 模型 NLPOutput 约束 LLM 输出,LLM 直接返回经过校验的对象:

python 复制代码
model = provider.get_model()
structured_model = model.with_structured_output(NLPOutput)
result: NLPOutput = await structured_model.ainvoke(messages)

一个容易被忽略的工程细节是 降级知识库 :当 LLM_API_KEY 未配置或调用失败时,analyze_repair_request 不会抛错,而是回退到一份关键词匹配的 mock 知识库 ------内置了路灯故障、道路破损、井盖异常、护栏损坏、环卫设施、交通信号设施、公共绿化等 7 类标准维修知识(维修步骤、所需工具、零件、安全提示)。这样即使 LLM 完全不可用,维修工在 H5 端仍能看到有用的作业指导,做到「降级不降质量」。

4.2 dispatch 节点:派单决策(核心亮点,详见第 5 节)

4.3 verify 节点:视觉验收 + 返工闭环

输入维修前照片、维修后照片与维修描述,调用视觉模型 qwen-vl-max 对比,输出 {verified, confidence, diff_summary}。关键在于它的失败语义

  • 验收通过verdict=True):worker_complete 继续执行通过分支------状态 repairing → verifying、记录 completed_at、ES 同步、Redis 同步、把维修员重新加回 Geo 候选池。
  • 验收不通过verdict=False):图的条件边路由到 rework_node,由它承担全部返工副作用------状态退回 repairingrepair_recordsai_rework_required 标记、推送返工通知、ES/Redis 同步、Geo 回池。
  • LLM 验收服务不可用 :保留「默认通过」的语义(verified=True, confidence=0.75),保证链路不因 AI 服务抖动而卡死。
python 复制代码
def _route_by_verdict(state: TicketState) -> str:
    """验收结果路由:未通过 → 返工节点;通过 → 结束。"""
    return "rework" if state.get("verdict") is False else "end"

5. 派单决策:硬约束 + 四维加权 + LLM 工具循环

派单是整条链路的核心亮点,实现为一个三层漏斗
#mermaid-svg-aKd6PN8Zii47iIo8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aKd6PN8Zii47iIo8 .error-icon{fill:#552222;}#mermaid-svg-aKd6PN8Zii47iIo8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aKd6PN8Zii47iIo8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aKd6PN8Zii47iIo8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aKd6PN8Zii47iIo8 .marker.cross{stroke:#333333;}#mermaid-svg-aKd6PN8Zii47iIo8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aKd6PN8Zii47iIo8 p{margin:0;}#mermaid-svg-aKd6PN8Zii47iIo8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 .cluster-label text{fill:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 .cluster-label span{color:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 .cluster-label span p{background-color:transparent;}#mermaid-svg-aKd6PN8Zii47iIo8 .label text,#mermaid-svg-aKd6PN8Zii47iIo8 span{fill:#333;color:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 .node rect,#mermaid-svg-aKd6PN8Zii47iIo8 .node circle,#mermaid-svg-aKd6PN8Zii47iIo8 .node ellipse,#mermaid-svg-aKd6PN8Zii47iIo8 .node polygon,#mermaid-svg-aKd6PN8Zii47iIo8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aKd6PN8Zii47iIo8 .rough-node .label text,#mermaid-svg-aKd6PN8Zii47iIo8 .node .label text,#mermaid-svg-aKd6PN8Zii47iIo8 .image-shape .label,#mermaid-svg-aKd6PN8Zii47iIo8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-aKd6PN8Zii47iIo8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aKd6PN8Zii47iIo8 .rough-node .label,#mermaid-svg-aKd6PN8Zii47iIo8 .node .label,#mermaid-svg-aKd6PN8Zii47iIo8 .image-shape .label,#mermaid-svg-aKd6PN8Zii47iIo8 .icon-shape .label{text-align:center;}#mermaid-svg-aKd6PN8Zii47iIo8 .node.clickable{cursor:pointer;}#mermaid-svg-aKd6PN8Zii47iIo8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aKd6PN8Zii47iIo8 .arrowheadPath{fill:#333333;}#mermaid-svg-aKd6PN8Zii47iIo8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aKd6PN8Zii47iIo8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aKd6PN8Zii47iIo8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aKd6PN8Zii47iIo8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aKd6PN8Zii47iIo8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aKd6PN8Zii47iIo8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aKd6PN8Zii47iIo8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aKd6PN8Zii47iIo8 .cluster text{fill:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 .cluster span{color:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aKd6PN8Zii47iIo8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aKd6PN8Zii47iIo8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-aKd6PN8Zii47iIo8 .icon-shape,#mermaid-svg-aKd6PN8Zii47iIo8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aKd6PN8Zii47iIo8 .icon-shape p,#mermaid-svg-aKd6PN8Zii47iIo8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aKd6PN8Zii47iIo8 .icon-shape .label rect,#mermaid-svg-aKd6PN8Zii47iIo8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aKd6PN8Zii47iIo8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aKd6PN8Zii47iIo8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aKd6PN8Zii47iIo8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} LLM 失败/越界
Redis Geo 半径检索
确定性硬约束过滤
高德驾车距离修正
四维加权评分 兜底
LLM 工具循环 增强
分布式锁 + 指派

5.1 第一层:Redis Geo 半径检索

维修员通过 /location 接口实时上报坐标到 workers:geo,派单时用 GEORADIUS 按故障设施坐标做半径筛选(普通工单 5km、紧急工单 10km,最多取 20 人):

python 复制代码
DEFAULT_SEARCH_RADIUS_KM = 5     # 默认半径
EMERGENCY_SEARCH_RADIUS_KM = 10  # 紧急工单半径
MAX_CANDIDATES = 20              # 候选上限

nearby_workers = await redis_geo.georadius(
    "workers:geo", facility_lng, facility_lat, radius_km,
    unit="km", withdist=True, withcoord=True,
    sort="ASC", count=MAX_CANDIDATES)

5.2 第二层:确定性硬约束(先于一切评分执行,LLM 无法绕过)

候选必须全部满足三条硬约束,任一违反即剔除:

  1. 日单上限today_orders < worker.max_daily_orders(当日已接单数未达上限);
  2. 夜班值守 :22:00--06:00 时段必须开启 night_duty
  3. 技能匹配worker.skills 与报修类别匹配。
python 复制代码
# 约束1:日单量上限
today_orders = int(await redis_counter.get(f"worker:{wid}:daily_order") or 0)
if today_orders >= worker.max_daily_orders:
    continue
# 约束2:夜班检查
if is_night and not worker.night_duty:
    continue
# 约束3:技能匹配
if worker.skills and not any(s.lower() in facility_lower or facility_lower in s.lower()
                             for s in skills_list):
    continue

紧急工单的放宽兜底 :当硬约束过滤后为空且是紧急工单,会以 _apply_hard_constraints_relaxed 放宽技能/夜班/日单上限(上限放宽至 1.5 倍)再试一次,保证紧急工单一定派得出去。

5.3 两段式距离:Redis Geo 直线 → 高德驾车

直线距离不能反映真实到达时间,因此引入两段式距离管道

  1. 粗筛:Redis Geo 直线距离筛出 ≤20 个候选(快、免费);
  2. 精修 :对候选逐一调高德驾车路径规划 driving_distance,替换为真实路面距离 + 预估耗时,结果带 5 分钟 Redis 缓存amap:driving:{worker_id}:{ticket_id})避免重复计费;
  3. 降级:高德失败时保留直线距离(或 Haversine 近似),绝不让外部 API 故障阻塞派单。

5.4 第三层:四维加权评分(确定性兜底)

评分维度与权重(dispatch_score_service.DEFAULT_WEIGHTS):

维度 权重 数据来源 归一化公式
distance 距离 40% 高德驾车距离(修正后) max(0, 100 - km*20)
load 负载 30% worker:{id}:daily_order 当日接单计数 max(0, 100 - orders*5)
rating 好评 20% worker:{id}:profilestar min(100, star*20)
response 响应 10% worker:{id}:profileavg_response_min max(0, 100 - min*5)
python 复制代码
total = (w["distance"]*dist_score + w["load"]*load_score
         + w["rating"]*rating_score + w["response"]*response_score) / 100.0

这个确定性加权算法是派单链路的最后一道保险------它不依赖任何外部服务,保证 LLM、高德、Redis 全部不可用时仍能产出可用的派单结果。

5.5 LLM 工具循环(增强,开关控制)

在确定性结果之上,当 LLM_ENABLE_DISPATCH_SCORING=True 时,dispatch 节点会进入 LLM 工具调用循环 ------这是全项目唯一用到 LangChain bind_tools 的地方,模型自主决策调用哪些工具:

工具 作用
search_candidates Geo 半径检索候选
get_worker_profile 读维修员画像 + 当日负载
get_driving_distance 高德驾车距离(带缓存)
submit_dispatch 终结决策 ,args 复用 DispatchScoreOutput schema
python 复制代码
llm = model.bind_tools([
    search_candidates, get_worker_profile, get_driving_distance, submit_dispatch,
])

循环最多 4 轮(MAX_TOOL_ITERATIONS = 4),模型在收集够信息后调用 submit_dispatch 提交最终选择。关键安全设计 ------LLM 的最终选择必须落在硬约束过滤后的合法候选池 valid_ids 内,否则回退确定性算法:

python 复制代码
valid_ids = {c["worker_id"] for c in candidate_pool}  # 硬约束过滤后的合法池
...
if final_choice is not None:
    if final_choice in valid_ids:
        return {"selected_worker_id": final_choice, "scores": final_scores}
    logger.warning(f"LLM 派单选择 {final_choice} 不在硬约束过滤后候选池中,回退确定性算法")
    return None

一句话总结这个设计:硬约束是确定性的「地板」,四维加权是确定性的「兜底」,LLM 工具循环是「天花板」------LLM 只被允许在硬约束划定的安全边界内做「更聪明的排序」,任何异常、超限、越界都回退到确定性结果。

5.6 指派:分布式锁 + MySQL 乐观锁双保险

无论走确定性还是 LLM 路径,落库副作用统一由 _assign_worker 承担,保证两条路径的副作用完全一致:

  1. SETNX 获取 lock:ticket:{id}(300s 自动过期防死锁);
  2. MySQL 更新 assigned_worker_id + status=dispatching + dispatched_at
  3. Redis 状态缓存同步 + 从接单大厅 zrem + Geo 移除该维修员;
  4. 创建派单通知、MongoDB 审计日志、发布 10 分钟超时检查消息、ES 同步。

抢单路径则另有 MySQL 层的 UPDATE ... WHERE status IN ('pending','accepting') 乐观锁兜底,防止 Redis 锁 TTL 过期等极端场景下的并发双接。


6. 响应速度指标:增量平均算法

「响应速度」被统一为一句话:从工单进入可派发状态到维修工首次响应的时长(分钟)。两种场景的起点不同:

场景 公式 写入点
抢单 accepted_at − created_at accept_ticket
派单 started_at − dispatched_at worker_checkin

为支持这一指标,tickets 表新增了 dispatched_at 字段,并实现了增量平均(避免每次全量重算历史):

txt 复制代码
new_avg = old × n/(n+1) + current × 1/(n+1)     # n = total_orders
python 复制代码
n = max(worker.total_orders or 0, 0)
old = float(worker.avg_response_minutes or 0.0)
new_avg = round((old * n + current_minutes) / (n + 1), 2)
worker.avg_response_minutes = new_avg
await db.commit()
# 同步 Redis profile,供派单评分「响应速度」维度实时读取
await redis_cache.hset(f"worker:{worker_id}:profile", "avg_response_min", str(new_avg))

这个指标形成了一个数据闭环:响应速度影响派单评分 → 派单评分决定派给谁 → 响应速度又被新工单更新。


7. 可靠性工程:锁、延迟队列与降级

7.1 RabbitMQ 延迟队列(DLX + per-message TTL)

「10 分钟无人接单自动派单」不靠定时轮询,而是用 RabbitMQ 的 延迟队列 :消息发布到 dispatch_timeout.delay 队列(expiration=600000),TTL 过期后由死信交换机(DLX)路由到 dispatch_timeout 被消费者处理。

python 复制代码
timeout_delay_queue = await _channel.declare_queue(
    QUEUE_DISPATCH_TIMEOUT_DELAY, durable=True,
    arguments={
        "x-dead-letter-exchange": EXCHANGE_NAME,
        "x-dead-letter-routing-key": QUEUE_DISPATCH_TIMEOUT,
    })

7.2 ES 同步的可靠投递:重试 + 指数退避 + DLQ

ES 属于「最终一致性」环节,因此设计成全量可靠异步投递es_sync 消费者失败后带 x-retry-count 头重新发布到延迟队列(指数退避 2s→120s),重试耗尽(5 次)路由到死信队列 es_sync.dlq 人工处理。消费者每次从 MySQL 全量加载工单再写 ES,避免部分字段覆盖问题。

7.3 全链路降级矩阵

依赖 故障时行为
LLM(百炼) NLP/验收走 mock 知识库;派单走确定性四维算法
高德驾车 API 回退 Redis Geo 直线距离 / Haversine
Redis Geo 返回空候选,报「半径内无在岗维修员」
MongoDB 日志/审计写失败仅 warning,不影响主流程
ES / RabbitMQ 工单已受理,ES 下次全量同步补回
MySQL 唯一同步阻断点,失败即回滚并返回错误

8. 数据存储设计

8.1 工单状态机

#mermaid-svg-ajue7QSW6pck4BGN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ajue7QSW6pck4BGN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ajue7QSW6pck4BGN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ajue7QSW6pck4BGN .error-icon{fill:#552222;}#mermaid-svg-ajue7QSW6pck4BGN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ajue7QSW6pck4BGN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ajue7QSW6pck4BGN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ajue7QSW6pck4BGN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ajue7QSW6pck4BGN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ajue7QSW6pck4BGN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ajue7QSW6pck4BGN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ajue7QSW6pck4BGN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ajue7QSW6pck4BGN .marker.cross{stroke:#333333;}#mermaid-svg-ajue7QSW6pck4BGN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ajue7QSW6pck4BGN p{margin:0;}#mermaid-svg-ajue7QSW6pck4BGN defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-ajue7QSW6pck4BGN g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-ajue7QSW6pck4BGN g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-ajue7QSW6pck4BGN g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-ajue7QSW6pck4BGN g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ajue7QSW6pck4BGN g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-ajue7QSW6pck4BGN .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ajue7QSW6pck4BGN .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-ajue7QSW6pck4BGN .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-ajue7QSW6pck4BGN .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ajue7QSW6pck4BGN .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-ajue7QSW6pck4BGN .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-ajue7QSW6pck4BGN .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-ajue7QSW6pck4BGN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ajue7QSW6pck4BGN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ajue7QSW6pck4BGN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ajue7QSW6pck4BGN .edgeLabel .label text{fill:#333;}#mermaid-svg-ajue7QSW6pck4BGN .label div .edgeLabel{color:#333;}#mermaid-svg-ajue7QSW6pck4BGN .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-ajue7QSW6pck4BGN .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-ajue7QSW6pck4BGN .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-ajue7QSW6pck4BGN .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-ajue7QSW6pck4BGN .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-ajue7QSW6pck4BGN .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ajue7QSW6pck4BGN .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ajue7QSW6pck4BGN #statediagram-barbEnd{fill:#333333;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ajue7QSW6pck4BGN .cluster-label,#mermaid-svg-ajue7QSW6pck4BGN .nodeLabel{color:#131300;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-ajue7QSW6pck4BGN .note-edge{stroke-dasharray:5;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-note text{fill:black;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram-note .nodeLabel{color:black;}#mermaid-svg-ajue7QSW6pck4BGN .statediagram .edgeLabel{color:red;}#mermaid-svg-ajue7QSW6pck4BGN #dependencyStart,#mermaid-svg-ajue7QSW6pck4BGN #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-ajue7QSW6pck4BGN .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ajue7QSW6pck4BGN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 市民报修
抢单(accepted_at)
10分钟超时自动派单(dispatched_at)
到场签到(started_at)
完工+AI验收通过(completed_at)
市民确认/7天自动(closed_at)
AI验收不通过(返工)
accepting
repairing
dispatching
verifying

tickets 表的关键时间戳字段:created_at / accepted_at / dispatched_at / started_at / completed_at / closed_at,六个时间戳完整刻画了工单全生命周期,也为响应速度、履约时效等指标提供了事实基础。

8.2 热状态缓存与 Geo

  • ticket:{id}:info(hash)------工单热状态,14 天 TTL,进度查询毫秒级;
  • tickets:accepting(zset)------接单大厅列表,按创建时间排序;
  • workers:geo------维修员实时位置,派单半径检索数据源。

8.3 MySQL 自动迁移

项目无 alembic,建表靠 Base.metadata.create_all。针对「新增列」这一痛点,mysql.py 里的 _ensure_columns() 会在启动时自动补齐 ORM 模型中新增但表中缺失的列 (如本次新增的 dispatched_at),避免了手动 ALTER 的运维负担。


9. 关键代码走读

9.1 派单选人(确定性)------dispatch_service._dispatch_selection

python 复制代码
# 状态守卫:超时派单只处理 accepting;普通派单处理 pending/dispatching
if is_timeout_dispatch:
    if ticket.status != "accepting":
        return fail(...)

# 半径随紧急程度放大
radius_km = EMERGENCY_SEARCH_RADIUS_KM if emergency else DEFAULT_SEARCH_RADIUS_KM
candidate_pool = await search_nearby_workers(redis_geo, facility_lng, facility_lat, radius_km)

# 硬约束过滤 + 紧急放宽
filtered = await _apply_hard_constraints(db, redis_counter, candidate_pool, facility_type)
if not filtered and emergency:
    filtered = await _apply_hard_constraints_relaxed(db, redis_counter, candidate_pool,
                                                      facility_type, relax_skill=True)
if not filtered:
    return fail("附近无可用维修员")

# 高德驾车修正
await _apply_driving_correction(filtered, redis_cache, facility_lng, facility_lat, ticket_id)

# 真实数据 + 四维评分
scoring_input = [{
    "worker_id": c["worker_id"],
    "distance_km": c["distance_km"],
    "today_orders": await _read_daily_orders(redis_counter, c["worker_id"]),
    "star_rating": profile["star_rating"],
    "avg_response_min": profile["avg_response_min"],
} for c in filtered]
score_result = await score_candidates(ticket_id, scoring_input, facility_lng, facility_lat)

9.2 确定性四维评分------dispatch_score_service._simple_score_candidates

python 复制代码
for c in candidates:
    distance_km = float(c.get("distance_km", 0) or 0)
    today_orders = int(c.get("today_orders", 0) or 0)
    star_rating = float(c.get("star_rating", 5.0) or 5.0)
    avg_response_min = float(c.get("avg_response_min", 0) or 0)

    dist_score = max(0, 100 - distance_km * 20)          # 越近越高,每公里扣 20
    load_score = max(0, 100 - today_orders * 5)          # 越闲越高,每单扣 5
    rating_score = min(100, star_rating * 20)            # 5 星 → 100
    response_score = max(0, 100 - avg_response_min * 5)  # 越快越高,每分钟扣 5

    total = (w["distance"]*dist_score + w["load"]*load_score
             + w["rating"]*rating_score + w["response"]*response_score) / 100.0

scored.sort(key=lambda x: x["total_score"], reverse=True)
return {"selected_worker_id": scored[0]["worker_id"], "scores": scored}

9.3 派单 LLM 工具循环------graph._llm_dispatch_loop

python 复制代码
async def _llm_dispatch_loop(ticket, candidate_pool):
    provider = get_llm_provider()
    if not provider.is_available():
        return None
    llm = provider.get_model().bind_tools([
        search_candidates, get_worker_profile, get_driving_distance, submit_dispatch,
    ])
    valid_ids = {c["worker_id"] for c in candidate_pool}  # 合法候选池

    messages = [SystemMessage(content=DISPATCH_TOOL_SYSTEM_PROMPT),
                HumanMessage(content=_build_dispatch_loop_user_text(ticket))]

    for _ in range(MAX_TOOL_ITERATIONS):
        response = await llm.ainvoke(messages)
        messages.append(response)
        for tc in getattr(response, "tool_calls", None) or []:
            if tc.get("name") == "submit_dispatch":
                final_choice = tc["args"].get("selected_worker_id")
                if final_choice in valid_ids:
                    return {"selected_worker_id": final_choice, ...}
                return None  # 越界 → 回退确定性
            result = await _run_tool(tc.get("name"), tc.get("args") or {})
            messages.append(ToolMessage(content=json.dumps(result, ensure_ascii=False, default=str),
                                        tool_call_id=tc.get("id")))
    return None  # 超限 → 回退确定性

9.4 事件触发图------main.handle_timeout

python 复制代码
if ticket.status == "accepting":
    # 仍在接单大厅,执行自动派单(触发 LangGraph 事件驱动短图 event=timeout)
    graph_result = await decision_graph.ainvoke(
        {"event": "timeout", "ticket_id": ticket_id},
        config={"configurable": {"db": db}},
    )
    dispatch_result = graph_result.get("dispatch_result") or {...}

10. 工程实践与踩坑总结

  1. 「确定性兜底 + LLM 增强」是 AI 应用落地的关键姿势:永远不要让 LLM 成为单点。本项目三层漏斗里,硬约束是确定性、四维评分是确定性,LLM 只是「可选增强」,且被约束在合法候选池内------任何 LLM 异常都不会让派单链路不可用。
  2. 节点边界要克制:LangGraph 节点应只对应「AI 决策点」,硬约束、评分、降级都是节点内部实现。把确定性逻辑强行抽象成节点,是常见的过度设计。
  3. 外部 API 的缓存与降级要成对出现:高德驾车距离既有 5 分钟缓存(省钱)又有直线回退(保命);LLM 既有结构化输出又有 mock 知识库。
  4. 并发安全靠「锁 + 乐观锁」双保险 :Redis SETNX 锁 + MySQL UPDATE ... WHERE status=... 乐观锁,两者互补,覆盖了锁 TTL 过期等极端场景。
  5. 状态归属要单一:工单状态流转的「事实」只在 MySQL,Redis 是缓存、MongoDB 是日志、图状态不持久化------避免同一事实在多处维护导致不一致。
  6. 异步环节要「最终一致 + 可补偿」:ES 同步设计了重试 + 指数退避 + DLQ,即使失败也能在下次全量同步补回。

11. 总结

本项目以「城市公共设施智能报修与派单」为场景,落地了一条 从报修解析、智能派单到视觉验收的完整 AI 决策链,核心亮点可以归纳为:

  • LangGraph 事件驱动短图:三节点决策链 + 条件边 + 返工闭环,无 checkpointer,轻量且契合天级业务生命周期;
  • 三层漏斗式派单:确定性硬约束(技能/夜班/日单上限)→ 四维加权评分(40/30/20/10)→ LLM 工具循环增强,LLM 不可用时无缝回退确定性算法;
  • 四库分层存储:MySQL 事务事实源、Redis 缓存/Geo/锁/计数、MongoDB 文档/日志、ES 搜索索引,各司其职;
  • 可靠性工程:延迟队列、分布式锁、指数退避重试、DLQ、全链路降级矩阵。
相关推荐
Hello-FPGA16 分钟前
量子计算滨松c15550-22up单光子相机与PCIe1004采集卡
人工智能·计算机视觉·fpga开发
DeepIntelli18 分钟前
企业独立站内容怎么改,才更容易被大模型引用:从语义化 HTML 到可抽取结构的工程化调整
人工智能·chatgpt
A.说学逗唱的Coke20 分钟前
【人工智能专题】Redis 杀进 AI 数据层:向量搜索、语义缓存与 Agent 记忆工程从入门到踩坑
人工智能·redis·缓存
luckystar513~22 分钟前
AI漫剧满天飞:创作工具全景指南
人工智能
harmony&23 分钟前
Docker 容器技术从入门到实战:生态系统、安装部署与架构详解
docker·容器·架构
十三画者24 分钟前
【文献分享】ExoFILT:基于深度学习的外泌事件单颗粒追踪数据分类器
人工智能·深度学习·机器学习·数据挖掘·数据分析
beiju29 分钟前
AI 改稿不该直接覆盖:从 Notion suggest edits 设计可审阅的 Patch 协议
人工智能
Summer-Bright30 分钟前
深度 | Hot Chips 2026 英特尔三响炮:256 核 Xeon、480GB 推理 GPU,赌 agentic 让 CPU 回归
人工智能·数据挖掘·回归·intel·hotchip
2601_9620739732 分钟前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
大数据·hadoop·架构