agent-architecture-report.html 34.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xlyAi Agent 架构报告</title>
<style>
  :root {
    --ink: #1a2332;
    --ink-2: #4a5568;
    --ink-3: #8494a8;
    --line: #dde4ec;
    --bg: #f7f9fb;
    --card: #ffffff;
    --blue: #2563eb;
    --blue-bg: #eff4ff;
    --green: #0e9f6e;
    --green-bg: #eafaf3;
    --amber: #b45309;
    --amber-bg: #fdf3e3;
    --red: #dc2626;
    --red-bg: #fdeeee;
    --purple: #7c3aed;
    --purple-bg: #f3efff;
    --teal: #0d9488;
    --teal-bg: #e8f7f5;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0; background: var(--bg); color: var(--ink);
    font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
    line-height: 1.75; font-size: 15px;
  }
  header.hero {
    background: linear-gradient(135deg, #16213a 0%, #1e3a5f 60%, #24507e 100%);
    color: #fff; padding: 44px 24px 36px;
  }
  .wrap { max-width: 1080px; margin: 0 auto; padding: 0 20px; }
  header.hero h1 { margin: 0 0 8px; font-size: 30px; letter-spacing: 1px; }
  header.hero p.sub { margin: 4px 0; color: #b9c8dd; font-size: 14px; }
  header.hero .meta { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 10px; }
  header.hero .meta span {
    background: rgba(255,255,255,.12); border: 1px solid rgba(255,255,255,.2);
    border-radius: 999px; padding: 3px 14px; font-size: 12.5px; color: #dce7f5;
  }
  nav.toc {
    background: var(--card); border-bottom: 1px solid var(--line);
    position: sticky; top: 0; z-index: 50;
  }
  nav.toc .wrap { display: flex; gap: 4px; overflow-x: auto; }
  nav.toc a {
    padding: 12px 16px; color: var(--ink-2); text-decoration: none;
    font-size: 14px; white-space: nowrap; border-bottom: 2.5px solid transparent;
  }
  nav.toc a:hover { color: var(--blue); border-bottom-color: var(--blue); }
  main { padding: 32px 0 70px; }
  section.part { margin-bottom: 52px; }
  .part-head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 6px; }
  .part-head .no {
    font-size: 13px; font-weight: 700; color: #fff; background: var(--blue);
    border-radius: 8px; padding: 3px 12px; letter-spacing: 1px;
  }
  .part-head h2 { margin: 0; font-size: 24px; }
  .part > .wrap > p.lead { color: var(--ink-2); margin-top: 4px; max-width: 860px; }
  h3 { font-size: 18px; margin: 32px 0 10px; padding-left: 10px; border-left: 4px solid var(--blue); }
  h4 { font-size: 15.5px; margin: 0 0 6px; color: var(--ink); }
  p { margin: 8px 0; }
  .card {
    background: var(--card); border: 1px solid var(--line); border-radius: 12px;
    padding: 16px 20px; margin: 12px 0;
  }
  .card p { font-size: 14px; color: var(--ink-2); }
  .grid { display: grid; gap: 12px; margin: 12px 0; }
  .grid.c2 { grid-template-columns: repeat(auto-fit, minmax(340px, 1fr)); }
  .grid.c3 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
  table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 10px; overflow: hidden; margin: 12px 0; font-size: 13.8px; }
  th { background: #eef2f7; text-align: left; padding: 9px 12px; font-weight: 600; color: var(--ink); border-bottom: 1.5px solid var(--line); white-space: nowrap; }
  td { padding: 9px 12px; border-bottom: 1px solid #edf1f6; vertical-align: top; color: var(--ink-2); }
  tr:last-child td { border-bottom: none; }
  td b, li b, p b { color: var(--ink); }
  code, .k {
    font-family: "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
    font-size: 12.5px; background: #edf1f7; color: #334561;
    border-radius: 5px; padding: 1px 6px;
  }
  .pill { display: inline-block; border-radius: 999px; padding: 1px 10px; font-size: 12px; font-weight: 600; }
  .pill.ro   { background: var(--green-bg); color: var(--green); }
  .pill.hitl { background: var(--purple-bg); color: var(--purple); }
  .pill.warn { background: var(--amber-bg); color: var(--amber); }
  .pill.risk { background: var(--red-bg); color: var(--red); }
  .pill.info { background: var(--blue-bg); color: var(--blue); }
  ul { margin: 6px 0; padding-left: 22px; }
  li { margin: 4px 0; color: var(--ink-2); font-size: 14px; }
  .note {
    border-left: 4px solid var(--amber); background: var(--amber-bg);
    border-radius: 0 10px 10px 0; padding: 10px 16px; margin: 12px 0; font-size: 14px; color: #6b4a10;
  }
  .note.green { border-left-color: var(--green); background: var(--green-bg); color: #0c6b4d; }

  /* 盒图 */
  .diagram { background: var(--card); border: 1px solid var(--line); border-radius: 12px; padding: 20px; margin: 14px 0; overflow-x: auto; }
  .diagram .title { font-size: 13px; color: var(--ink-3); text-align: center; margin-bottom: 12px; letter-spacing: 2px; }
  .diagram .caption { font-size: 13.5px; color: var(--ink-3); margin: 8px 4px 0; }
  .lane-row { display: flex; align-items: stretch; justify-content: center; gap: 0; min-width: 760px; }
  .lane {
    flex: 1; border: 1.5px solid var(--line); border-radius: 12px; padding: 12px;
    background: #fbfcfe; margin: 0 8px; min-width: 200px;
  }
  .lane .lane-title { font-weight: 700; text-align: center; font-size: 14px; margin-bottom: 8px; color: var(--ink); }
  .lane .lane-sub { text-align: center; font-size: 11.5px; color: var(--ink-3); margin: -6px 0 8px; }
  .box {
    border-radius: 8px; padding: 7px 10px; margin: 7px 0; font-size: 12.8px;
    border: 1px solid var(--line); background: #fff; color: var(--ink-2); text-align: center;
  }
  .box.b { background: var(--blue-bg); border-color: #c4d5f5; color: #1e40af; }
  .box.g { background: var(--green-bg); border-color: #b6e6d3; color: #066a4a; }
  .box.p { background: var(--purple-bg); border-color: #ddd0fa; color: #5b21b6; }
  .box.t { background: var(--teal-bg); border-color: #b5e2dd; color: #0b6b62; }
  .box.a { background: var(--amber-bg); border-color: #f2dcb3; color: #92510a; }
  .box.r { background: var(--red-bg); border-color: #f3c6c6; color: #a11d1d; }
  .box small { display: block; font-size: 11px; opacity: .8; margin-top: 1px; }
  .conn { display: flex; align-items: center; justify-content: center; color: var(--ink-3); font-size: 12px; flex-direction: column; padding: 0 2px; min-width: 54px; }
  .conn .arrow { font-size: 20px; line-height: 1; color: #9fb0c4; }

  /* 流程条 */
  .flow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 10px 0; }
  .flow .step {
    background: #fff; border: 1.5px solid var(--line); border-radius: 10px;
    padding: 8px 13px; font-size: 13px; color: var(--ink-2); text-align: center; line-height: 1.45;
  }
  .flow .step b { display: block; font-size: 13px; color: var(--ink); }
  .flow .step.hl { border-color: #c4d5f5; background: var(--blue-bg); }
  .flow .step.gate { border-color: #ddd0fa; background: var(--purple-bg); }
  .flow .step.stop { border-color: #f2dcb3; background: var(--amber-bg); }
  .flow .step.done { border-color: #b6e6d3; background: var(--green-bg); }
  .flow .fa { color: #9fb0c4; font-size: 17px; }

  /* 状态机 */
  .fsm { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; margin: 12px 0; }
  .fsm .st { border-radius: 10px; padding: 10px 16px; text-align: center; border: 1.8px solid; font-size: 13px; line-height: 1.5; }
  .fsm .st b { font-size: 14px; display: block; }
  .fsm .st small { font-size: 11.5px; display: block; opacity: .85; }
  .fsm .pend { border-color: #c4d5f5; background: var(--blue-bg); color: #1e40af; }
  .fsm .succ { border-color: #b6e6d3; background: var(--green-bg); color: #066a4a; }
  .fsm .fail { border-color: #f3c6c6; background: var(--red-bg); color: #a11d1d; }
  .fsm .fa { color: #9fb0c4; font-size: 18px; }

  /* 记忆分段 */
  .memseg { display: flex; flex-direction: column; gap: 7px; margin: 10px 0; }
  .memseg .seg { border-radius: 10px; padding: 8px 15px; font-size: 13.5px; border: 1.5px solid; }
  .memseg .seg b { display: inline-block; margin-right: 8px; }
  .memseg .tag { float: right; font-size: 11.5px; opacity: .75; }
  .memseg .s1 { border-color: #c4d5f5; background: var(--blue-bg); color: #1e40af; }
  .memseg .s2 { border-color: #ddd0fa; background: var(--purple-bg); color: #5b21b6; }
  .memseg .s3 { border-color: #f2dcb3; background: var(--amber-bg); color: #92510a; }
  .memseg .s4 { border-color: #b5e2dd; background: var(--teal-bg); color: #0b6b62; }
  .memseg .s5 { border-color: #b6e6d3; background: var(--green-bg); color: #066a4a; }

  footer { color: var(--ink-3); font-size: 12.5px; text-align: center; padding: 26px 0 40px; border-top: 1px solid var(--line); }
  @media (max-width: 720px) { .lane-row { min-width: 0; flex-direction: column; } .conn { transform: rotate(90deg); padding: 6px 0; } }
</style>
</head>
<body>

<header class="hero">
  <div class="wrap">
    <h1>xlyAi Agent 架构报告</h1>
    <p class="sub">印刷 ERP 智能助手 —— 单 ReAct Agent · 技能驱动 · 模型全只读 · 人在环写入</p>
    <p class="sub">依据 agent-main 分支当前代码编写,与设计文档不一致处以代码为准。</p>
    <div class="meta">
      <span>Java / LangChain4j</span>
      <span>本地 27B 模型(数据不出网)</span>
      <span>MySQL + Redis</span>
      <span>7 个工具 · 5 项技能</span>
      <span>2026-07-29</span>
    </div>
  </div>
</header>

<nav class="toc">
  <div class="wrap">
    <a href="#p1">一、Agent 设计</a>
    <a href="#p2">二、核心机制</a>
    <a href="#p3">三、AI ⇄ ERP 协作</a>
    <a href="#p4">四、质量与风险</a>
  </div>
</nav>

<main>

<!-- ================================================================ Part 1 -->
<section class="part" id="p1">
  <div class="wrap">
    <div class="part-head"><span class="no">PART 1</span><h2>Agent 的设计</h2></div>
    <p class="lead">xlyAi 是 ERP 之上的「理解与编排」层:听懂业务人员的话,替他们在 1700 多张表单里查数、看单、发起操作。它是<b>一个</b>智能体(无路由、无意图分类),且<b>模型本身没有任何写入能力</b>——改数据的永远是人的一次点击。</p>

    <h3>1.1 四条设计立场</h3>
    <div class="grid c2">
      <div class="card">
        <h4>① 单智能体,自己决定怎么做</h4>
        <p>所有对话进同一个 ReAct 循环,模型自行决定查数、澄清、载入流程还是出卡。不设意图门——实测无准确率收益,反而在分类环节丢失原话信息。</p>
      </div>
      <div class="card">
        <h4>② 模型全只读,写入是人的动作</h4>
        <p>模型最多把写操作渲染成一张带按钮的卡片,然后停下;真正写入走用户点按钮后的确定性路径,不经过模型。幻觉在结构上碰不到数据。</p>
      </div>
      <div class="card">
        <h4>③ 知识外置:流程是数据,不是代码</h4>
        <p>业务流程写成技能文本存在数据库里,模型按需载入照做;改流程不用改代码发版。领域结构(域、表单、字段)来自离线物化的知识图谱。</p>
      </div>
      <div class="card">
        <h4>④ 薄层:业务权威永远在 ERP</h4>
        <p>读走 ERP 与网页相同的 API,写只投递到一张队列表由 ERP 执行。AI 层不复制业务规则,越薄越不会与 ERP 真实行为漂移。</p>
      </div>
    </div>

    <h3>1.2 Agent 解剖</h3>
    <div class="diagram">
      <div class="title">大 脑 · 知 识 · 能 力 · 记 忆 · 边 界</div>
      <div class="lane-row">
        <div class="lane">
          <div class="lane-title">知识</div>
          <div class="box t">业务域地图<small>11 个域及流转,常驻提示词</small></div>
          <div class="box t">技能库<small>流程 playbook,按需载入</small></div>
          <div class="box t">知识图谱<small>表单目录 · 字段字典</small></div>
        </div>
        <div class="conn"><span class="arrow"></span></div>
        <div class="lane" style="border-color:#c4d5f5;">
          <div class="lane-title">大脑 + 循环</div>
          <div class="box b">本地 27B 模型(低温度)</div>
          <div class="box b">ReAct 工具循环<small>自选工具与时机,最多 8 步</small></div>
          <div class="box b">唯一 system prompt<small>角色+域地图+技能索引+硬规则</small></div>
          <div class="box r">确定性护栏<small>反编造重试 · 谎报纠正</small></div>
        </div>
        <div class="conn"><span class="arrow"></span></div>
        <div class="lane">
          <div class="lane-title">能力(全部只读/只渲染)</div>
          <div class="box g">查:找表单 · 读数据 · 查记录</div>
          <div class="box p">问:澄清小问题(可点选项)</div>
          <div class="box p">请求核准:弹表单 / 出预览卡<small>出卡即停,等人点按钮</small></div>
        </div>
      </div>
      <div class="lane-row" style="margin-top:10px;">
        <div class="lane" style="flex: 0 1 50%;">
          <div class="lane-title">记忆</div>
          <div class="box t">会话事件账本(唯一事实源)</div>
          <div class="box t">按 token 预算投影成上下文(见 2.3)</div>
        </div>
        <div class="lane" style="flex: 0 1 50%;">
          <div class="lane-title">边界</div>
          <div class="box a">身份:ERP token 透传 + 服务端内省,失败即拒</div>
          <div class="box a">权限:表单级白名单 + 租户隔离,随每次调用注入</div>
        </div>
      </div>
      <p class="caption">权限不是模型要遵守的规则,而是注入在每个工具里、它无法越过的边界。</p>
    </div>

    <h3>1.3 一轮对话的生命周期</h3>
    <div class="diagram">
      <div class="flow">
        <div class="step"><b>用户发话</b>自由文本</div>
        <div class="fa"></div>
        <div class="step hl"><b>确定身份</b>token 换真实身份与权限<br>失败即拒</div>
        <div class="fa"></div>
        <div class="step hl"><b>组装上下文</b>提示词 + 流程卡 + 记忆投影</div>
        <div class="fa"></div>
        <div class="step hl"><b>ReAct 循环</b>载技能 / 查数 / 澄清 / 出卡</div>
        <div class="fa"></div>
        <div class="step gate"><b>护栏检查</b>没查数报数字→重试<br>谎称已保存→纠正</div>
        <div class="fa"></div>
        <div class="step done"><b>收尾</b>给出答复,或出卡即停</div>
      </div>
      <p class="caption">全程流式推送;每个事件按序落入账本,成为下一轮记忆。用户点了卡片按钮,则进入第二部分的确定性写路径。</p>
    </div>

    <h3>1.4 能力面:七个工具,四种角色</h3>
    <div class="note green"><b>工具集固定且极小</b>:所有轮次共用同一组 7 个工具与同一份提示词。能力面越小,小模型的工具选择越稳,提示前缀稳定也利于推理缓存。</div>
    <table>
      <tr><th>角色</th><th>工具</th><th>做什么 / 为什么</th></tr>
      <tr><td><span class="pill info">载入流程</span></td><td><code>useSkill</code></td><td>载入某技能全文。提示词只放一行索引,全文按需加载;载入后钉在流程卡上跨轮生效。</td></tr>
      <tr><td rowspan="3"><span class="pill ro">感知查数</span></td><td><code>findForms</code></td><td>「某类单据」→ 具体哪张表单。模型不必背 1748 张表结构。</td></tr>
      <tr><td><code>readFormData</code></td><td>读某表单一页数据(可过滤、翻页)。答案里的每个数字都必须出自这里,输出截断分页。</td></tr>
      <tr><td><code>lookupRecord</code></td><td>某条命名记录的完整字段。多条命中报候选让用户挑,不猜。</td></tr>
      <tr><td><span class="pill hitl">对话交互</span></td><td><code>askUser</code></td><td>一个澄清问题 + 可点选项。缺关键信息才问、一次问全、问完即停。</td></tr>
      <tr><td rowspan="2"><span class="pill hitl">请求核准</span></td><td><code>collectForm</code></td><td>新增:弹一张表单让用户一次填齐;已说的值预填,客户/产品从真实数据下拉选。</td></tr>
      <tr><td><code>previewChange</code></td><td>改/作废/审核等:出预览卡(当前值 + 变化高亮 + ERP 同名按钮)。人核对的就是将要写入的。</td></tr>
    </table>

    <h3>1.5 知识体系三层</h3>
    <div class="grid c3">
      <div class="card">
        <h4>域地图(常驻)</h4>
        <p>11 个业务域及上下游流转,每域一行常驻提示词——给模型一张「问题落在哪」的粗地图。</p>
      </div>
      <div class="card">
        <h4>技能(按需载入)</h4>
        <p>每篇只写步骤与停止条件,不写错例枚举。内置五篇:查询、新建报价、新建单据、修改记录、单据状态操作。</p>
      </div>
      <div class="card">
        <h4>知识图谱(工具背后)</h4>
        <p>从 ERP 元数据物化的表单目录与字段字典(中文名⇄技术列⇄外键)。模型不直接看它,但每次定位与解析都踩在它上面。</p>
      </div>
    </div>

    <h3>1.6 环境</h3>
    <div class="diagram">
      <div class="lane-row">
        <div class="lane">
          <div class="lane-title">浏览器</div>
          <div class="box">聊天流 + 卡片/表单/选项片</div>
          <div class="box">多命名会话侧栏</div>
        </div>
        <div class="conn"><span class="arrow"></span><span>流式对话<br>ERP 登录态透传</span></div>
        <div class="lane" style="border-color:#c4d5f5;">
          <div class="lane-title">xlyAi(本报告主角)</div>
          <div class="box b">单 ReAct Agent</div>
          <div class="box p">确定性保存端点(人在环写)</div>
          <div class="box a">授权层(补 ERP 缺失的表单级权限)</div>
        </div>
        <div class="conn"><span class="arrow"></span><span>只读 API<br>+ 共享队列表</span></div>
        <div class="lane">
          <div class="lane-title">ERP 后端(业务权威)</div>
          <div class="box">表单读 API · 身份内省 · 字段候选</div>
          <div class="box a">写操作执行器 + AI 待办 + 状态回执</div>
        </div>
      </div>
      <p class="caption">支撑设施:MySQL(与 ERP 共库,多租户)、Redis(缓存/暂存)、本地 27B 模型(OpenAI 兼容通道)。接口细节见第三部分,实现映射见附录。</p>
    </div>
  </div>
</section>

<!-- ================================================================ Part 2 -->
<section class="part" id="p2">
  <div class="wrap">
    <div class="part-head"><span class="no">PART 2</span><h2>核心机制</h2></div>
    <p class="lead">支撑上述设计的四条机制:写路径的层层闸门、事件账本记忆、以及针对小模型不可靠性的确定性护栏。</p>

    <h3>2.1 编排:技能文本代替路由与流水线</h3>
    <div class="card">
      <ul>
        <li><b>规则只有一条</b>:全部自由文本进唯一 ReAct agent,流程知识由模型按需 useSkill 载入。</li>
        <li><b>不做意图门</b>:分类器不掌握字段定义,抽取必有损(「大16开」→「16开」);原话只应在字段定义存在的地方被解释一次。</li>
        <li><b>跨轮续办靠流程卡</b>:激活的技能全文钉在提示词尾部,流程终结才摘下——旧轮被压缩也不丢步骤。</li>
      </ul>
    </div>

    <h3>2.2 写路径:三道闸</h3>
    <div class="note"><b>LLM 说什么都改不了数据库。</b>能写的只有两个确定性端点,且最多写一行待办——执行在 ERP 侧。</div>
    <div class="diagram">
      <div class="title">以「修 改 记 录」为 例</div>
      <div class="flow">
        <div class="step"><b>用户</b>「把 XX 单数量改成 3000」</div>
        <div class="fa"></div>
        <div class="step hl"><b>模型(只读)</b>载技能 → previewChange</div>
        <div class="fa"></div>
        <div class="step gate"><b>闸① 预览卡</b>定位 + 校验<br>解析产物暂存服务端</div>
        <div class="fa"></div>
        <div class="step stop"><b>模型停手</b>等用户核对</div>
      </div>
      <div class="flow">
        <div class="step"><b>用户点【保存】</b></div>
        <div class="fa"></div>
        <div class="step gate"><b>闸② 确定性端点</b>归属/权限重查 → <b>重读记录重校验</b><br>→ 原子抢占防重复提交</div>
        <div class="fa"></div>
        <div class="step gate"><b>闸③ 入队</b>ai_op_queue 一行 pending</div>
        <div class="fa"></div>
        <div class="step done"><b>ERP 执行</b>报价自动执行<br>其余进待办</div>
      </div>
    </div>
    <div class="grid c2">
      <div class="card">
        <h4>所见即所写</h4>
        <p>保存时重读记录:被他人改过的值、变了的审核/作废状态一律拒绝,请重新预览。卡上显示的 FK 是实际解析绑定的记录名、数字是规范化后的值——人核对的就是入库的。</p>
      </div>
      <div class="card">
        <h4>状态合法性硬检查</h4>
        <p>审核要求未审核、作废要求未作废等前置条件在预览与保存各查一次,ERP 执行时刻再查一次,三层防竞态。</p>
      </div>
      <div class="card">
        <h4>新增走表单</h4>
        <p>用户填完点保存后由端点构建载荷:中文名→列、FK 名称→id、类型强转(解析不了的数字报错而非写 0)、补必填默认、生成主键与单号。报价单构建主表+从表+多数量三张表,价格留给 ERP 核价。</p>
      </div>
      <div class="card">
        <h4>防重复防并发</h4>
        <p>previewId 一次性、原子抢占,并发点击只有一个能入队;多字段先全部过校验再单事务入队,绝不写一半。</p>
      </div>
    </div>

    <h3>2.3 记忆:事件账本 + 四段投影</h3>
    <div class="card">
      <p>会话没有可被并发覆盖的「聊天记录数组」:每句话、每次工具调用、每次按钮点击都是一条<b>追加事件</b>,落 MySQL(Redis 热缓存)。前端历史与 LLM 上下文是同一账本的两种投影,永不对不上。喂给模型的上下文按 token 预算分段组装:</p>
      <div class="memseg">
        <div class="seg s1"><b>① system prompt</b><span class="tag">永不让位</span></div>
        <div class="seg s2"><b>② 进行中的流程卡</b>激活技能 + 在办单据状态<span class="tag">永不让位</span></div>
        <div class="seg s3"><b>④ 往事摘要区</b>预算外旧轮 → 每轮一行确定性摘要<span class="tag">小额预算</span></div>
        <div class="seg s4"><b>⑤ 近期原文区</b>整轮纳入,旧轮工具结果压成短摘要<span class="tag">预算内尽量装</span></div>
        <div class="seg s5"><b>当前轮</b>原样保真,超限时先压最早的工具结果<span class="tag">永不让位</span></div>
      </div>
      <p>发送前自检总量,超了继续收缩——绝不让推理端从头部静默截断(那样 system prompt 先死)。估算故意偏高,并与模型返回的实际 token 数持续对账。</p>
    </div>

    <h3>2.4 针对小模型的确定性护栏</h3>
    <div class="grid c2">
      <div class="card"><h4>反编造</h4><p>零工具调用却答出数字 → 注入纠正话术自动重试一次;复发则标注「未经核实」。</p></div>
      <div class="card"><h4>谎报纠正</h4><p>声称「已保存/已完成」但本轮没出过卡 → 追加提示「没有卡片即没有任何写入」。</p></div>
      <div class="card"><h4>确定性解析</h4><p>尺寸拆分(「50*30*5」→长/宽/高)、数字/日期强转、FK 消歧、单号生成全在代码层——模型负责理解意图,代码负责解释取值。</p></div>
      <div class="card"><h4>资源护栏</h4><p>工具循环最多 8 步、超时 180 秒、所有工具输出分页/截断。</p></div>
    </div>

    <h3>2.5 安全红线</h3>
    <table>
      <tr><th>不变量</th><th>一句话</th></tr>
      <tr><td><b>身份 fail-closed</b></td><td>身份只来自 ERP 服务端内省;token 无效即 401,绝不回落管理员身份。</td></tr>
      <tr><td><b>token 不进模型</b></td><td>用户 token 仅单次请求持有,绝不进提示词、账本、暂存。</td></tr>
      <tr><td><b>表单级白名单</b></td><td>读/写/下拉共用同一权限边界;鉴权参数用服务端反查值,不信模型自报。</td></tr>
      <tr><td><b>租户隔离</b></td><td>所有查询强制品牌谓词,拿不到租户宁可返回空。</td></tr>
      <tr><td><b>会话隔离</b></td><td>会话 id 强制用户命名空间——伪造他人 id 只会落到自己名下。</td></tr>
      <tr><td><b>注入防护</b></td><td>URL id 白名单校验、SQL 全参数化、CORS 白名单。</td></tr>
      <tr><td><b>读不变写</b></td><td>读接口只传分页/过滤参数,绝不透传 ERP 存在的写类参数。</td></tr>
    </table>
  </div>
</section>

<!-- ================================================================ Part 3 -->
<section class="part" id="p3">
  <div class="wrap">
    <div class="part-head"><span class="no">PART 3</span><h2>AI 侧 ⇄ ERP 侧:协作与接口</h2></div>
    <p class="lead">分界线一句话:<b>xlyAi 负责理解、定位、校验、请求核准;ERP 负责执行、审计、权属、回执。</b>交接靠同步的薄 HTTP 接口 + 异步的共享队列表。</p>

    <h3>3.1 HTTP 接口(xlyAi → ERP,全部透传用户 token)</h3>
    <table>
      <tr><th>接口</th><th>用途</th></tr>
      <tr><td><code>GET /ai/whoami</code></td><td>token → 服务端认定的身份与租户,所有端点的身份唯一来源。</td></tr>
      <tr><td><code>POST /business/getBusinessDataByFormcustomId</code></td><td>表单数据读取,与网页前端同一接口,天然带租户与行级权限。</td></tr>
      <tr><td><code>GET /getUuid</code></td><td>新增载荷的主键由 ERP 发号。</td></tr>
      <tr><td><code>POST /business/checkBusinessData</code></td><td>dry-run 预检:复用 ERP 真实校验链、强制回滚,把「保存了才发现不合法」提前。</td></tr>
      <tr><td><code>POST /ai/fieldOptions</code></td><td>FK 下拉候选,按 ERP 下拉配置取——带行级权限、级联过滤、联动回填;不可用时回落本地字典。</td></tr>
    </table>

    <h3>3.2 ai_op_queue:交接面与状态协议</h3>
    <div class="card">
      <p>用户点按钮后 xlyAi 写一行操作请求(类型 + 目标 + 载荷 + 描述),状态恒为 <b>pending+100</b>;领取、执行、幂等、审计、回写全在 ERP 侧。现行策略:<b>报价自动执行,其余进「我的 AI 待办」</b>。xlyAi 只把状态只读展示在流程卡上,绝不重复提交。</p>
      <div class="fsm">
        <div class="st pend"><b>pending</b><small>100 已提交(xlyAi 唯一写入)</small><small>101 ERP 处理中</small></div>
        <div class="fa"></div>
        <div class="st succ"><b>success</b><small>200 成功 · 201 幂等命中</small></div>
        <div class="fa" style="margin: 0 10px;">/</div>
        <div class="st fail"><b>fail</b><small>400 载荷 · 401 权限 · 404 不存在 · 409 状态冲突</small><small>422 业务校验 · 423 锁定 · 450 取消 · 500/504</small></div>
      </div>
      <ul>
        <li>xlyAi 只写 pending+100,其余状态只由 ERP 回写。</li>
        <li>一次保存多字段 = 多行待办(单事务);ERP 执行时刻再验状态,冲突回 409。</li>
      </ul>
    </div>

    <div class="diagram">
      <div class="title">职 责 分 界</div>
      <div class="lane-row">
        <div class="lane" style="border-color:#c4d5f5;">
          <div class="lane-title">xlyAi</div>
          <div class="box b">理解意图 · 载入技能 · 定位表单与记录</div>
          <div class="box b">校验:字段/类型/FK/权限/状态/租户</div>
          <div class="box p">渲染卡片,等人点按钮</div>
          <div class="box a">入队 pending —— <b>到此为止</b></div>
        </div>
        <div class="conn"><span class="arrow"></span><span>ai_op_queue</span></div>
        <div class="lane">
          <div class="lane-title">ERP 侧</div>
          <div class="box">执行器以用户身份复用 ERP 原生写入/审核逻辑</div>
          <div class="box">幂等 · 执行时刻校验 · 单号执行前重新生成</div>
          <div class="box">审计留痕 · 状态回写 · AI 待办入口</div>
        </div>
      </div>
    </div>

    <h3>3.3 身份与授权协作</h3>
    <div class="grid c2">
      <div class="card">
        <h4>凭证透传,不自造身份</h4>
        <p>xlyAi 不签发、不存储、不续期任何用户凭证:前端逐请求带 ERP token,xlyAi 原样转发。会话按稳定身份存,重登录换 token 后历史接得回。</p>
      </div>
      <div class="card">
        <h4>授权互补</h4>
        <p>ERP 后端只强制租户 + 行级两层,表单级权限校验处于关闭状态(仅前端 UI 用)。xlyAi 用同一份权限数据在自己侧补回这层,作为纵深防御;根因整改待业务决策。</p>
      </div>
    </div>
  </div>
</section>

<!-- ================================================================ Part 4 -->
<section class="part" id="p4">
  <div class="wrap">
    <div class="part-head"><span class="no">PART 4</span><h2>质量保障 · 残余风险 · 设计取舍</h2></div>

    <h3>4.1 质量保障</h3>
    <div class="grid c3">
      <div class="card"><h4>基准评测</h4><p>50 题冻结基线 + 多轮轨迹基准(消歧/接续/夹带/回指),与生产读同一份提示词和技能文件。当前:扩展集 12/12,全轨迹 84%。</p></div>
      <div class="card"><h4>单元测试</h4><p>8 个测试类钉住不变量:fail-closed、会话归属、投影正确性、并发不丢事件、强转拒非法值等。</p></div>
      <div class="card"><h4>对抗性审计</h4><p>多智能体并行审计 + 独立复核证否,最近一次全量复审 CRITICAL 0 / HIGH 3,HIGH 已全部修复回归。</p></div>
    </div>
    <div class="card"><h4>可观测性</h4><p>每次 LLM 调用记录耗时/token/错误,估算与实际 token 持续对账;可选导出自托管 Langfuse(数据不出网)。业务审计随执行权在 ERP 侧。</p></div>

    <h3>4.2 残余风险(交付说明)</h3>
    <table>
      <tr><th>级别</th><th>事项</th><th>一句话</th></tr>
      <tr><td><span class="pill risk">安全</span></td><td>技能表是提示词注入面</td><td>谁能写 ai_skill 谁就能改 agent 行为,需收紧表写权限。</td></tr>
      <tr><td><span class="pill risk">安全</span></td><td>dry-run 失败放行</td><td>刻意取舍:预检挂了不挡用户,ERP 执行时仍会真校验。</td></tr>
      <tr><td><span class="pill warn">安全</span></td><td>token URL 通道 / 生产前置校验</td><td>壳层嵌入的 token 传递与 nginx 前置会话校验属环境决策项。</td></tr>
      <tr><td><span class="pill warn">运维</span></td><td>账本永久保留</td><td>append-only 永不删除,需定保留/归档策略。</td></tr>
      <tr><td><span class="pill warn">协作</span></td><td>队列旧列待删 / 生产迁移待执行</td><td>等 ERP 去掉兼容读后删列;4 份迁移 SQL 待上生产。</td></tr>
      <tr><td><span class="pill warn">协作</span></td><td>「处理中」卡死无恢复</td><td>ERP 领取后异常退出会停在 101,需 ERP 侧超时补偿。</td></tr>
      <tr><td><span class="pill info">根因</span></td><td>ERP 权限根因未整改</td><td>xlyAi 的白名单是纵深防御,不替代后端重开表单级校验。</td></tr>
      <tr><td><span class="pill info">功能</span></td><td>无语义召回</td><td>表单定位全靠关键词匹配,模糊说法要换词重试。</td></tr>
      <tr><td><span class="pill info">功能</span></td><td>策展字段仅报价有</td><td>其他复杂主-从单据的新增走字典启发式,体验有限。</td></tr>
    </table>

    <h3>4.3 贯穿全局的取舍</h3>
    <ul>
      <li><b>不引编排框架</b>:人在环用「出卡即停 + 确定性端点」实现,比中断/续跑机制更简单,也更安全——模型根本没有写工具。</li>
      <li><b>可靠性放代码不放提示词</b>:不写逐案纠错规则,靠确定性解析 + 廉价恢复 + 回归基准。</li>
      <li><b>执行权交出去</b>:AI 止步于入队,写入发生在 ERP 原生路径——代码量与攻击面同时缩小。</li>
      <li><b>处处幂等</b>:所有交接点都假设会被重复触发(一次性抢占、追加原子、幂等键、单号重生成)。</li>
    </ul>

    <h3>附录:设计概念 → 代码映射</h3>
    <table>
      <tr><th>设计概念</th><th>主要代码位置</th></tr>
      <tr><td>ReAct 循环 / 按请求组装</td><td><code>agent/ReActAgent</code> · <code>config/AgentFactory</code> · <code>agent/AgentIdentity</code></td></tr>
      <tr><td>对话入口 / SSE / 护栏</td><td><code>web/AgentChatController</code></td></tr>
      <tr><td>7 个工具</td><td><code>tool/</code>:UseSkillTool · KgQueryTool · ErpReadTool · FormCollectTool · PreviewChangeTool · InteractionTool</td></tr>
      <tr><td>提示词 / 技能</td><td><code>service/SystemPromptService</code><code>prompts/system.txt</code>)· <code>service/SkillService</code><code>ai_skill</code> / <code>skills/*.md</code></td></tr>
      <tr><td>表单/字段/FK 解析校验</td><td><code>service/FormResolverService</code> · <code>service/FormRenderService</code><code>viw_ai_useful_forms</code> / <code>viw_kg_field_dict</code></td></tr>
      <tr><td>预览/保存闭环 / 唯一写入口</td><td><code>service/PreviewService</code> · <code>web/PreviewController</code> · <code>service/OpService</code><code>ai_op_queue</code></td></tr>
      <tr><td>账本与记忆投影</td><td><code>service/LedgerService</code><code>ai_chat_event</code>)· <code>agent/EventLogChatMemory</code> · <code>service/EventProjectionService</code></td></tr>
      <tr><td>身份与授权 / ERP 客户端</td><td><code>service/AuthzService</code> · <code>service/ErpClient</code></td></tr>
      <tr><td>会话 / FK 选项</td><td><code>service/ConversationService</code> · <code>web/ConversationController</code> · <code>web/FormController</code></td></tr>
      <tr><td>可观测性</td><td><code>config/TracingChatModelListener</code></td></tr>
    </table>
  </div>
</section>

</main>

<footer>
  xlyAi Agent 架构报告 · 2026-07-29 · 依据 agent-main 分支当前代码 · 与设计文档不一致处以代码为准
</footer>

</body>
</html>