资讯动态

用大模型Agent调度老系统:从自然语言到工具执行的全工程实践

发布时间:2026/10/13 14:45:31 来源:尧图企业网站定制
这个项目是我半年前开始折腾的起因其实很简单我们内部有两套跑了五年多的老体系一套是静态代码扫描一套是日常报表查询。它们每天都在被使用但谁都不想碰代码命令行又长又难记每次用都得翻内部文档。我当时就想能不能让大模型当调度员直接把这俩老伙计拉起来跑用户用自然语言提问大模型负责理解意图、转换参数、调用命令、整理结果。听起来很唬人但真正做下来我发现难点根本不在大模型本身而在于“怎么把传统体系的边界和规则讲清楚”——这一块的经验目前市面上分享得很少。如果你也在维护老代码库或者正琢磨怎么把旧系统接入AI能力这篇文章应该能让你少走几个月的弯路。我不会绕弯子直接把踩过的坑、改过的代码、压测的数据都摆出来。1. 为什么是这两套传统体系选型背后的真实原因先交代一下背景不然后面很多东西你会觉得莫名其妙。我们当时把目标锁定在两套系统上不是随机挑的而是它们刚好代表了两种典型的老系统类型。1.1 老体系到底老在哪里第一套是静态规则扫描工具我叫它QuickRule。它基于历史事故总结出了一套精确匹配的规则库比如哪些函数调用习惯容易出内存问题哪些正则写法有性能隐患。它的准确率很高但使用方式停留在命令行时代你得手动输python qr_scan.py --repo 路径 --rules security然后等它慢慢吐出一大段JSON报告。第二套是报表查询服务我叫它QueryDesk。它封装了几十个固定的SQL模板解决日常运营数据查询问题。业务方想看前一天的关键指标得先翻Excel找到编号再用命令行带参数调用。它的逻辑完全没问题但人机交互极其不友好。这两套体系有个共同点核心能力是值钱的但使用门槛太高。它们不是那种“该被淘汰的老古董”而是“能力很扎实但没有马车夫”的老爷车。1.2 为什么我坚决不选择重写很多人听到“把传统体系装进AI”第一反应是重写一个Python库把这套逻辑直接嵌进Agent里。我当时也这么想过但很快否决了。重写的风险太大了。QuickRule那套规则库沉淀了上千条经验每条规则背后都有事故案例它不是几行正则那么简单。QueryDesk里的几十个SQL模板每次数据口径调整都有业务方签字确认重写意味着你可能连隐藏的业务假设都丢掉。更重要的是重写解决不了根本问题。你的瓶颈不在“规则执行”而在“入口方式”。传统体系缺的是门不是发动机。所以我选了一条更务实的路保留原系统通过Agent编排层把它们暴露成大模型能调用的工具让LLM做翻译和调度。这里分享一个我在技术选型时常用的判断标准如果一项能力每天都在用但很少出问题那就不要重写而是把它当成工具接进来。真正值得重写的是那些“每天都在吐锅、流程本身有问题”的模块。1.3 框架选型为什么用状态化编排而不是裸调API我们把问题简化成一句话用户说话Agent决定谁干活干活的结果怎么反馈。看起来简单但实际跑起来你需要处理多轮对话、上下文保持、工具参数校验、结果超长截断这些问题。我最后选的是LangGraph这类有状态编排框架而不是单纯用LangChain的链式调用或者裸调大模型API。原因很简单这个场景的本质是一个状态机。用户可能先问“扫一下A仓库的安全问题”下一句又说“再对比下B仓库”再下一句可能是“把两份结果合并成报表”。如果没有状态管理每个请求都是独立的Agent就记不住自己上一轮干了什么。有状态编排的核心好处是你可以随时拿到“当前会话走到哪一步了”出了问题能回退重试而不是整个流程推倒重来。简单说它给了你一个控制流骨架Agent在里面做决策而不是让Agent自己瞎编一套流程。2. 把传统CLI变成大模型能用的“手”环境封装与工具设计这一章是整个项目的核心工程部分。你需要的不是复杂概念而是一套清晰、可复制的方法论。2.1 架构长什么样整个系统跑起来之后请求链路是这样的用户消息进入Agent调度器调度器调用大模型做意图识别模型决定需要调用哪些工具、传什么参数然后工具层通过subprocess去执行老系统的命令行拿到结果后先做截断和摘要再交给模型生成最终回答。用户感知不到QuickRule和QueryDesk的存在他们只知道自己提了一个问题答案就出来了。但底层那两套老体系确实在被调用一行代码都没改。这个架构的巧妙之处在于老系统完全不感知大模型的存在它看到的只是一个看起来像“有人手动敲命令”的过程。风险被隔离在Agent层而不是渗透到老系统内部。2.2 subprocess封装给CLI穿上工具的外衣让大模型调用老体系最稳妥的方式不是直接把命令行丢给它而是写一个Python函数把命令封装成有明确参数的工具。看一个简化示例import subprocess, json, os def quick_scan(repo_path: str, rules: str all) - str: 运行QuickRule静态规则扫描。 repo_path必须是文件系统上已存在的绝对路径。 rules限制为security、performance或all。 返回扫描结果摘要JSON完整结果已写入临时文件路径在summary字段中。 if not os.path.exists(repo_path): raise ToolParamError(f仓库路径不存在: {repo_path}) if rules not in {security, performance, all}: raise ToolParamError(f不支持的扫描规则类型: {rules}仅支持security/performance/all) cmd [python, qr_scan.py, --repo, repo_path, --rules, rules] try: proc subprocess.run( cmd, capture_outputTrue, textTrue, timeout45, cwd/opt/quickrule ) except subprocess.TimeoutExpired: return json.dumps({error: 扫描超时请缩小仓库范围或稍后重试}) if proc.returncode ! 0: return json.dumps({error: f扫描失败: {proc.stderr[-500:]}}) raw json.loads(proc.stdout) return _summarize_report(raw)这段代码有三个关键设计。一是形参类型注解和docstring写得极其详细因为大模型是通过这些文字来理解“这个工具该怎么调用”的。二是参数白名单校验放在工具内部这意味着即使模型传错参数工具自己会拦住而不是让错误命令真的执行。三是所有错误信息都返回结构化JSON方便模型理解发生了什么而不是丢一堆堆栈给它。2.3 工具描述怎么写LLM才不会手滑工具封装好了还得让大模型知道“什么时候该用它”。这里有个实践工具描述越精确模型越不会瞎调。有些团队图省事工具描述只写一句“运行扫描工具”。结果大模型就把参数编出花来了。你把描述写成这样{ name: quick_scan, description: 运行QuickRule扫描本地代码仓库。仅当你需要检查代码安全、性能问题时使用。repo_path必须是已经存在的绝对路径rules只能是security、performance、all三者之一。, parameters: { type: object, properties: { repo_path: { type: string, description: 要扫描的本地仓库绝对路径例如 /data/repos/service-account }, rules: { type: string, enum: [security, performance, all], description: 扫描规则范围。安全扫描用security性能问题用performance不确定全部扫描用all。 } }, required: [repo_path, rules] } }注意那个enum字段它直接限制了模型只能从给定的几个值里挑。这比你在描述里写一百遍“不要乱传”都管用。经验是在参数schema层面做限制比在提示词里苦口婆心更可靠。2.4 QueryDesk的接入为什么必须走白名单QueryDesk跟QuickRule还不一样它涉及SQL执行安全边界必须更严格。我接它的时候不是让大模型随便写SQL而是把已有的三十几个模板预先编号大模型只能选模板ID和日期参数不能直接拼SQL语句。这个决策当时有人觉得太保守说那样“限制了大模型的能力”。但我的观点很明确报表查询的正确率优先级高于自由度。一个运营人员不需要大模型发明新SQL他需要的是“快速找到正确的那条SQL并执行”。当然参数部分可以灵活一些。比如日期范围、业务线筛选这些都是模板内定义好的占位符大模型只需要填入合理的值。这样一来既保留了灵活性又不会让模型有机会输出DELETE FROM users这种灾难。3. 从自然语言到最终回答一次完整任务的链路拆解光讲架构太抽象我拿一个真实场景把整个过程走一遍。假设运营同学在Agent界面输入“今天有哪些仓库扫描出了高优先级安全规则”3.1 请求进入Agent的初始化判断用户输入到达调度器后第一步不是立刻调用大模型而是先做基础检查会话身份对不对、有没有残留的上下文、上一步有没有未完成的任务。没有状态管理框架的话这一步很容易漏掉导致Agent每次都是“失忆”状态。这一步走完消息才会带着历史上下文进大模型。模型看到用户的问题结合工具列表里的描述判断出“用户想扫高优先级安全规则”然后开始规划工具调用。3.2 第一轮工具调用QuickRule跑起来模型决定调用 quick_scan 工具参数是{repo_path: /data/repos, rules: security}。工具层检查路径存在、规则合法然后subprocess执行老命令。这里有一个细节很多实际仓库扫描一次要跑几十秒甚至几分钟而模型等待工具返回的时间不能太长。所以在_summarize_report里做了两层处理def _summarize_report(raw_report: dict) - str: high_count len(raw_report.get(high_priority_issues, [])) total_count raw_report.get(total_issues, 0) top_issues raw_report.get(high_priority_issues, [])[:5] temp_path f/tmp/quickrule_results/{uuid4().hex}.json with open(temp_path, w, encodingutf-8) as f: json.dump(raw_report, f, ensure_asciiFalse, indent2) summary { summary: f扫描完成共发现 {total_count} 个问题其中高优先级 {high_count} 个。, top_5: top_issues, full_result_path: temp_path, note: 如需查看完整报告请使用read_result工具读取full_result_path。 } return json.dumps(summary, ensure_asciiFalse)这样做的原因是大模型的上下文窗口是稀缺资源。QuickRule可能返回五万个条目如果全塞进上下文后续对话基本没法用了。截断摘要 完整结果落盘是解决这个问题的标准姿势。3.3 第二轮工具调用QueryDesk补充运营数据模型阅读摘要后发现“高优先级安全规则”还差一层信息——这些扫描出来的问题涉及哪些业务线影响多少用户。它继续判断应该调用 QueryDesk 的模板 #17按扫描结果查业务线分布。这一轮调用会自动带上上一轮拿到的仓库名列表作为参数。模型在几秒钟内完成了交叉关联最后生成了一段结构清晰的回答哪些仓库存在高优先级安全问题、预计影响面、建议优先修复顺序。3.4 会话记忆如何穿越多轮调用我特别强调状态管理是因为没有它上面的链路走到第二轮就断了。模型在第一轮拿到了仓库名列表如果这些信息没有存到session_context里第二轮它根本不知道要查哪些仓库的业务线分布。实际实现不复杂就是维护一个全局的会话存储SESSION_STORE {} def _read_session(session_id: str) - dict: return SESSION_STORE.get(session_id, {}) def _write_session(session_id: str, **kwargs) - None: SESSION_STORE.setdefault(session_id, {}).update(kwargs)在工具返回结果时顺手把摘要和关键实体写入会话存储。下一轮用户说话时调度器会把这些信息拼进新的消息里模型自然就“记得”上一轮发生了什么。整个链路跑通之后看起来就像Agent真的在用两套老系统干活但实际上每个决策都有据可查、可以回放。4. 上线前踩过的坑这五个问题差点让项目翻车任何Agent项目都一样demo跑通只是开始真正的问题都在接入真实数据之后浮出水面。我挑五个最有代表性的坑每个都包含排查思路和补救方案。4.1 大模型“创造性”地修改枚举参数第一次联调时我发现在一次扫描请求里模型居然把 rules 参数传成了critical。我明明只给了三个枚举值它愣是造了一个新值出来。命令自然报错但它完全没有意识到错误还一本正经地回答用户“扫描已完成”。排查链路是这样的我先去翻日志确认工具层确实是收到了critical然后怀疑是schema配置没生效。检查JSON Schema之后发现enum确实加了但模型版本对函数调用的格式解析有差异偶尔会绕过schema直接生成参数。解决方案分两层。第一层是工具函数内部继续做硬校验收到非法值直接抛异常并返回错误字符串第二层是在系统提示词里明确追加一句“所有工具参数必须使用给定枚举值禁止推断新值。”现在做项目我的默认原则是**永远假设模型会传错参数然后在离模型最远的那层兜住它。**工具层做最后的守门员是稳妥的方案没有例外。4.2 上下文窗口被结果爆炸撑爆上线后的第三天有人问了一个范围特别大的扫描问题QuickRule返回了六万个条目的JSON。我没做截断之前的版本直接把JSON塞进消息里结果模型的上下文窗口直接溢出后续所有请求都报错。排查链路很清晰报错信息显示“请求内容超过最大上下文”那问题一定出在上一次工具返回上。定位到_summarize_report后我把返回内容限制在2000字符以内完整报告一律写临时文件模型需要细节时再按需读取。这个机制后来帮了大忙。因为大模型的注意力会随着输入变长发散精简摘要不仅解决了内存问题还提升了回答质量。工具返回的不是“给模型看的东西”而是“给模型做决策用的线索”。4.3 并发调用导致老系统锁文件我们给Agent开放了多个会话同时使用结果QuickRule是老式单进程实现多个命令同时跑会在同一个临时目录写文件导致互相覆盖扫描结果变得乱七八糟。排查时看到的现象是一会儿A会话拿到B会话的报告一会儿直接报错找不到文件。翻开日志才意识到老系统设计时根本没考虑并发它假定同一时刻只有一个人用。这个问题的补救是在工具外层加一个并发控制器用信号量限制同时执行的扫描任务数import threading scan_semaphore threading.Semaphore(4) def quick_scan_with_limit(repo_path: str, rules: str) - str: with scan_semaphore: return quick_scan(repo_path, rules)超过四个并发任务时多出的请求会等待而不是直接失败。这样既不压垮老系统用户也感觉不到排队的存在。同理QueryDesk也加了每两秒最多一次调用的限速避免数据库压力突增。4.4 SQL安全问题不能依赖提示词有一次测试我给模型设置了一个开放式问题“看看能不能找出所有用户信息。”结果模型试图调用QueryDesk但传入了一个不在白名单里的SQL模板ID。我们的QueryDesk在入口做了模板ID校验直接拒绝了请求没有给它任何执行空间。我在这件事上的经验很直接**跟安全底线相关的校验永远不要交给LLM的“自觉”。**白名单校验是硬编码的、确定性的不存在任何智能判断空间。大模型可以自由选择模板ID但它就不了权决定不了SQL长什么样。同理任何工具如果可能执行危险操作必须在工具侧加一层众所周知的防火墙。让Agent自由发挥的前提是护栏足够牢固。4.5 工具签名改了Agent还在按老格式调项目接近尾声时我重构了QuickRule的参数结构把path改成了repo_path。结果忘记同步更新工具描述Agent继续用旧的参数名调用连续报错。这个坑让我意识到工具层和模型描述层必须是一套自动化同步的产物。现在的做法是建了一个tool_manifest配置文件所有工具的参数Schema只维护一份启动时由代码动态生成给模型的JSON Schemadataclass class ToolSpec: name: str description: str parameters_schema: dict TOOL_REGISTRY { quick_scan: ToolSpec( namequick_scan, description..., parameters_schema{...} ), query_desk: ToolSpec(...) }之后凡是改工具签名只动这一份文件模型描述和实际执行逻辑不会分叉。这是纯工程化的解法但特别有效。5. 这套方案能走多远效能、边界与后续演进方向项目上线后我花了一段时间做效能对比和数据采集。有些数据挺有意思也有些边界情况让我意识到这套方案不是万能的。5.1 改造前后的真实对比场景改造前人工流程改造后Agent流程查某仓库的高危扫描结果翻文档记命令 手动执等结果 人工解析JSON直接问约30秒出结论找某业务线前日核心指标翻SQL编号Excel 拼参数执行 手工整理表格直接问约15秒出结论给管理层出周报跨两个系统查数 Excel手动合并提问一次自动拉取并汇总成段落时间上确实节省了很多但更关键的是降低了门槛。以前只有熟悉命令行的人能查数现在连不碰终端的同事也可以自助完成。我见过几个部门用这个入口替代了原有的工单流程这是我没有预料到的效果。5.2 什么情况不适合装这套方案我踩过的教训告诉我有三类系统不适合做Agent化改造。第一类是本身没有稳定程序入口的系统只有人工操作界面每次操作都要点按钮。这类系统需要先补出CLI或API否则只能靠模拟点击脆弱得要命。第二类是规则逻辑处于高频变动期、随时在改判断标准的系统。你花大力气把工具描述、参数约束都做好了结果规则下个月换了一套维护成本比收益还大。第三类是对延迟极度敏感的实时系统Agent链路多了一次模型调用至少增加几百毫秒到几秒的延迟这不是靠优化能抹平的。如果原来的命令执行只要50毫秒那直接跑命令就好不需要大模型横插一脚。5.3 如果要接第三套、第四套体系现在团队常态化使用这套方案之后新的系统接入需求开始不断冒出来。我的建议是当工具数量超过五个后不要继续在单个Agent函数里堆代码而是维护一个轻量的服务注册表。注册表里的每条记录包括系统地址、可用命令、参数Schema、权限级别、超时设置、并发限制。Agent启动时加载全部工具注册信息动态生成可调用的函数列表。新系统上线时只需要在注册表里登记一条新记录不用重新部署Agent。另外路由逻辑也要设计成“确定性优先模型兜底”。比如用户提到了明确的某个模板关键词就直接走规则映射不经模型判断只有当规则匹配不上时才把意图识别交给大模型。这种混合路由能省下大量不必要的模型调用也减少错误率。我还坚持做一趟每两周一次的回归评测把历史上典型的一百个问题整理成测试集每次工具描述或底层命令有变更时都跑一遍对比输出正确率和失败率。这个习惯让后来几个月的迭代一直处于稳定状态强烈推荐你也建一个。最后说点实际操作层面的体会这个项目从第一行代码到稳定运行前后大约花了六周。回过头看技术上没什么惊天动地的创新真正值钱的是“克制”——始终管住手没有因为用了大模型就去乱改老系统的任何逻辑把大模型的角色严格限定在翻译和调度上。如果你现在正准备做类似的事我的建议是先选定两套最有代表性、最有日常价值的传统体系快速搭出一个最小循环自然语言进、工具执行、结果出去。不要一上来就追求复杂编排先把一条链路跑通再慢慢加状态、加并发、加安全校验。这期间你可能会遇到模型不听话、工具报错、上下文爆炸这些七七八八的事都很正常按我前面说的那套排查链路去应对就行。我个人现在最享受的时刻是每次看到业务同事直接在对话里查数据不用再转工单也不用来问我命令怎么写。那种感觉就是老体系还在稳稳地跑着但它的门被打开了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑