1. 项目概述:从“一问一答”到“持续规划”的跨越
最近在做一个智能客服系统的升级项目,客户提了个挺有意思的需求:他们希望系统不仅能回答单次问题,还能像经验丰富的技术支持专家一样,在复杂的故障排查、方案咨询等场景下,与用户进行多轮、有逻辑的对话,并最终能生成一个清晰的架构图或流程图,把讨论的方案可视化出来。这让我意识到,传统的“输入-输出”式对话模型已经不够用了,我们需要的是一个具备“思考-规划-执行-反思”闭环能力的智能体,也就是现在常说的Agent。
这个“多轮对话生成架构图 Agent”的核心价值在于,它模拟了人类专家在解决复杂问题时的思维过程。想象一下,当你向一位架构师咨询系统设计时,他不会立刻给你一张图。他会先问你业务规模、技术栈偏好、预算约束;然后基于你的回答,在脑中构思几个候选方案;接着,他会和你讨论每个方案的优劣,可能还会追问一些细节;最后,才把达成共识的方案画出来。我们要做的,就是用代码和算法把这个过程自动化。这不仅仅是接个 ChatGPT API 那么简单,它涉及到对话状态管理、任务分解、工具调用、以及最终的图形化渲染等多个环节的协同。无论是做智能客服、设计助手,还是教育领域的解题引导,这个模式都有很大的用武之地。
2. 核心设计思路:构建一个会“思考”和“使用工具”的智能体
设计这样一个 Agent,关键在于让它摆脱“鹦鹉学舌”式的回复,具备自主的任务理解和分步执行能力。我的设计思路围绕“规划-执行-反思”这个核心循环展开,并为其配备必要的“工具箱”。
2.1 智能体的“大脑”:基于大语言模型的规划与决策
Agent 的“大脑”通常由一个大型语言模型(LLM)担任,例如 GPT-4、Claude 3 或开源的 Llama 3 等。它的核心职责不是直接生成最终答案,而是进行任务规划和工具调度。
理解与规划:当用户提出一个初始请求,比如“帮我设计一个能承受百万日活的电商系统”,LLM 首先需要理解这个模糊的请求背后隐含的多个子任务。它会规划出一个大致的步骤序列,例如:
- 步骤一:澄清需求(询问具体的业务模块、数据量级、一致性要求等)。
- 步骤二:选择合适的技术组件(如负载均衡器、数据库选型、缓存策略)。
- 步骤三:确定组件间的交互关系和数据流向。
- 步骤四:调用绘图工具,生成架构图。
- 步骤五:解释生成架构图的关键设计点。
工具调用:LLM 知道自己不擅长画图、精确计算或查询实时信息。因此,在规划中,它会识别出需要外部工具协助的步骤。例如,“确定数据库选型”可能需要调用一个知识库查询工具来获取最新的数据库性能对比数据;“生成架构图”则明确需要调用绘图工具。LLM 会生成结构化的工具调用请求,包含工具名称和输入参数。
注意:这里的一个常见误区是让 LLM 直接生成 PlantUML 或 Mermaid 代码。对于复杂架构,LLM 很容易生成语法错误或逻辑矛盾的代码。更好的做法是让 LLM 输出一个结构化的中间表示(如 JSON),描述节点和边,再由一个专用的、稳定的转换器去生成图形代码。这解耦了逻辑和渲染,提升了系统的鲁棒性。
2.2 智能体的“工具箱”:为对话与绘图赋能
一个强大的 Agent 离不开一套好用的工具。我为这个项目设计了几类核心工具:
对话状态管理工具:这是实现多轮对话的基石。它需要维护一个“对话上下文”,不仅包括历史消息,更重要的是维护当前任务的状态机。例如,状态可能是“需求收集进行中”、“技术方案讨论中”、“等待用户确认细节”、“绘图生成中”。这个工具帮助 Agent 记住之前聊过什么,当前处在哪个阶段,下一步该做什么。我通常会用 Redis 或数据库来持久化这些会话状态,键值包括
session_id,current_stage,collected_parameters(收集到的用户需求),conversation_history等。需求澄清与追问工具:这是一个封装好的提示词(Prompt)模板。当 LLM 判断当前信息不足以进行下一步时(例如,用户只说“要一个高可用系统”),该工具会触发一组预设的、针对性的问题。例如:“请问您对‘高可用’的具体要求是几个9的可用性(如 99.9% 或 99.99%)?”、“故障恢复时间目标(RTO)和恢复点目标(RPO)分别是多少?”。
架构知识检索工具:为了让推荐的技术栈更专业、更与时俱进,Agent 需要访问一个架构知识库。这个知识库可以是你维护的 Markdown 文档、向量数据库(如 Chroma, Weaviate)中存储的最佳实践案例,甚至是联网搜索的权限。当 LLM 需要决定是使用 Kafka 还是 RabbitMQ 时,它可以调用此工具,查询两者的适用场景、吞吐量对比和运维复杂度。
架构图生成工具:这是最终的输出环节。输入是经过多轮对话后形成的、结构化的系统描述(JSON 格式)。这个工具负责将其转换为图形定义语言。
- 方案A(推荐):
结构化JSON -> 模板引擎 -> Mermaid/PlantUML 代码 -> 渲染服务。这样 LLM 只需输出规整的 JSON,降低了出错率。 - 方案B:
LLM -> Mermaid/PlantUML 代码 -> 渲染服务。更直接,但对 LLM 的代码生成能力要求高,需要后置严格的语法检查和修正。 渲染服务可以使用mermaid.ink这样的在线 API,或者自部署mermaid-cli、PlantUML server来生成 PNG/SVG 图片。
- 方案A(推荐):
2.3 工作流与状态机设计
整个 Agent 的工作流是一个动态循环,我将其设计为一个可扩展的状态机。
初始状态:等待用户输入 | v 用户输入 -> LLM 分析意图并规划下一步 | v 判断:信息是否充足? | |-- 否 -> 调用【追问工具】,进入“澄清中”状态,等待用户回复。 | |-- 是 -> 判断:下一步是否需要调用工具? | |-- 是 -> 执行工具调用(如检索知识库),更新对话状态和上下文。 | | | v | LLM 分析工具返回结果,并规划下一步。 | |-- 否 -> LLM 直接生成回复给用户。 | v 判断:是否满足生成架构图的条件?(如需求已确认,组件已选定) | |-- 是 -> 调用【绘图工具】,生成架构图并解释。 | | | v | 任务完成,状态重置或进入总结阶段。 | |-- 否 -> 返回,继续下一轮对话。这个状态机的核心是“LLM 作为调度中心”。每一步之后,LLM 都会根据当前完整的上下文(历史对话+工具执行结果+当前状态)重新评估,决定下一步行动。这赋予了 Agent 处理意外中断、用户修改需求等复杂情况的能力。
3. 关键技术实现细节与踩坑实录
有了设计思路,接下来就是具体的实现。这部分我会分享几个关键模块的代码级细节和实际开发中遇到的“坑”。
3.1 与大语言模型的高效交互:超越简单的 Chat Completion
很多人调用 LLM API 就是简单的user: ...,assistant: ...对话。但对于 Agent,我们需要更精细的控制。我采用ReAct (Reasoning + Acting)范式来构建提示词。
系统提示词(System Prompt)是 Agent 的“角色设定”和“行为准则”,必须写得非常明确:
你是一个资深的系统架构师助手。你的目标是通过多轮对话,帮助用户厘清需求,并最终生成一个清晰的技术架构图。 ## 能力 1. 你可以通过调用工具来获取信息或执行任务。 2. 你会主动、有条理地询问不清楚的需求细节。 3. 你最终会生成一个结构化的架构描述,用于绘图。 ## 行为规则 - 每次回复,你必须首先思考当前对话阶段和用户目标。 - 如果你需要更多信息,请直接提出具体问题。 - 如果你有足够信息进行下一步或调用工具,请按以下格式输出: 【思考】<你的推理过程> 【行动】<工具名称> 【输入】<传递给工具的JSON参数> - 如果你可以直接回答用户问题,请正常回复。 - 架构图生成条件是:核心业务模块、数据存储、外部依赖三者都已明确。用户提示词(User Prompt)则包含了当前的对话历史、工具返回结果和当前状态。这里的关键是上下文管理。我们不能无限制地把所有历史对话都塞给 LLM,因为存在令牌(Token)限制。我的策略是:
- 维护一个固定长度的“短期记忆”(如最近10轮对话)。
- 维护一个“摘要式长期记忆”:每经过一定轮次,或用 LLM 对之前的对话进行关键信息摘要,并将摘要放入后续对话的上下文。这能有效节省 Token,并聚焦关键信息。
一个踩过的坑:早期我把所有工具的描述都放在系统提示词里,导致提示词过长且混乱。后来我改成了“动态工具描述”:只在 LLM 可能需要调用工具时,才在当轮的用户提示词中附加相关工具的功能和参数说明。这大大提高了提示词的效率和 LLM 对工具理解的准确度。
3.2 工具调用模块的实现:稳定与灵活并重
工具调用模块是 Agent 的“手和脚”。我将其设计为一个可插拔的注册中心。
class ToolRegistry: def __init__(self): self._tools = {} def register(self, name: str, func: callable, description: str, schema: dict): """注册一个工具""" self._tools[name] = { 'function': func, 'description': description, 'schema': schema # JSON Schema,描述输入参数 } async def execute(self, tool_name: str, arguments: dict) -> str: """执行指定工具""" if tool_name not in self._tools: return f"Error: Tool '{tool_name}' not found." tool = self._tools[tool_name] try: # 验证参数是否符合 schema(此处省略验证代码) result = await tool['function'](**arguments) return str(result) except Exception as e: return f"Error executing tool '{tool_name}': {str(e)}" # 示例:注册一个架构知识查询工具 def query_arch_knowledge(keyword: str, aspect: str = "comparison") -> str: # 模拟从向量数据库或知识库中查询 knowledge_base = { ("kafka", "comparison"): "Kafka 适用于高吞吐、持久化、流式数据场景,支持多订阅者...", ("rabbitmq", "comparison"): "RabbitMQ 适用于复杂的路由、消息确认、低延迟场景..." } return knowledge_base.get((keyword.lower(), aspect), "No relevant knowledge found.") registry = ToolRegistry() registry.register( name="query_arch_knowledge", func=query_arch_knowledge, description="查询关于特定技术组件(如数据库、中间件)的架构知识、选型对比或最佳实践。", schema={ "type": "object", "properties": { "keyword": {"type": "string", "description": "技术组件名称,如 kafka, redis"}, "aspect": {"type": "string", "enum": ["comparison", "best_practice", "scaling"], "default": "comparison"} }, "required": ["keyword"] } )当 LLM 输出【行动】query_arch_knowledge和【输入】{"keyword": "Kafka"}时,Agent 的核心循环就会截取这些信息,调用ToolRegistry.execute()方法,并将工具返回的结果(如“Kafka 适用于高吞吐...”)插入下一轮对话的上下文,供 LLM 参考。
实操心得:工具函数的返回值最好是结构化的字符串或简单 JSON。避免返回过于复杂或冗长的 HTML/Markdown,因为 LLM 需要“阅读”这个结果。另外,每个工具都应该有完善的错误处理和超时机制,防止单个工具失败导致整个 Agent 卡死。
3.3 从结构化描述到架构图:可靠的内容生成
这是用户体验的临门一脚。如前所述,我不建议让 LLM 直接生成图形代码。我的流水线是这样的:
结构化输出约束:在最终生成图的阶段,我会在给 LLM 的提示词中严格要求其输出格式。
请根据以上对话,总结出最终的架构设计,并严格按照以下 JSON 格式输出,不要有任何额外解释: { "system_name": "电商系统", "components": [ {"id": "client", "type": "external", "label": "Web/App 客户端"}, {"id": "nginx", "type": "gateway", "label": "Nginx 负载均衡"}, {"id": "app_server", "type": "service", "label": "应用服务器集群"}, {"id": "redis", "type": "cache", "label": "Redis 缓存"}, {"id": "mysql_master", "type": "database", "label": "MySQL 主库"}, {"id": "mysql_slave", "type": "database", "label": "MySQL 从库"} ], "connections": [ {"from": "client", "to": "nginx", "label": "HTTPS 请求"}, {"from": "nginx", "to": "app_server", "label": "负载均衡"}, {"from": "app_server", "to": "redis", "label": "缓存读写"}, {"from": "app_server", "to": "mysql_master", "label": "写操作"}, {"from": "app_server", "to": "mysql_slave", "label": "读操作"} ] }使用 JSON 模式,极大提高了 LLM 输出的规整度。
模板化渲染:收到 JSON 后,用一个预定义的 Jinja2 模板将其转换为 Mermaid 代码。
{# mermaid_template.j2 #} graph TD {% for comp in components %} {{ comp.id }}[“{{ comp.label }}”] {% endfor %} {% for conn in connections %} {{ conn.from }} -->|“{{ conn.label }}”| {{ conn.to }} {% endfor %}渲染后得到:
graph TD client[“Web/App 客户端”] nginx[“Nginx 负载均衡”] app_server[“应用服务器集群”] redis[“Redis 缓存”] mysql_master[“MySQL 主库”] mysql_slave[“MySQL 从库”] client -->|“HTTPS 请求”| nginx nginx -->|“负载均衡”| app_server app_server -->|“缓存读写”| redis app_server -->|“写操作”| mysql_master app_server -->|“读操作”| mysql_slave后端渲染与返回:将生成的 Mermaid 代码发送给后端渲染服务(如调用
mermaid.ink或自建服务),得到图片 URL,最终连同架构描述一起返回给前端展示。
这样做的好处:即使未来想更换绘图引擎(比如从 Mermaid 切换到 Draw.io 或 Excalidraw),也只需要修改模板和渲染步骤,核心的 Agent 逻辑和 LLM 交互完全不用动。
4. 工程化部署与性能优化考量
当原型验证通过后,要投入实际使用,就必须考虑工程化的问题。这里面的水一点也不浅。
4.1 会话、状态与上下文的持久化
Agent 是有状态的,而且对话可能很长。你不能把所有状态都放在内存里。我的方案是:
- 会话存储:使用 Redis 或 PostgreSQL。每个活跃会话一个 Key,存储序列化后的会话对象。Redis 更快,适合高频访问;PostgreSQL 更可靠,便于复杂查询和分析。
- 状态管理:会话对象中不仅包含原始对话记录,更重要的是一个
state字典。这个字典记录了当前任务进展到了哪一步、收集到了哪些关键参数(如{“availability”: “99.99%”, “peak_qps”: “10000”})。这比让 LLM 每次都从历史对话中自行推断要可靠和高效得多。 - 上下文窗口优化:这是成本(Token 费用)和效果(记忆力)的平衡。我采用分层记忆策略:
- 工作记忆:最近 3-5 轮对话的原始记录,保证对最新话题的连贯理解。
- 摘要记忆:每隔一定轮次(或当对话历史过长时),用一个小模型(如 GPT-3.5-turbo)或专门的摘要链,将之前的对话浓缩成一段“背景摘要”。
- 关键事实记忆:将用户明确确认的需求参数(如“我们需要 MySQL 数据库”、“必须使用微服务”)提取出来,作为独立的事实点存储。 在每次构造 LLM 提示词时,将“背景摘要”和“关键事实”放在系统提示词或上下文头部,后面跟上“工作记忆”。这样可以用最少的 Token 携带最核心的信息。
4.2 降低延迟与提升响应速度的技巧
Agent 需要多次调用 LLM 和工具,容易导致响应慢。用户体验是“这 AI 怎么老在思考?”。
- 流式输出(Streaming):在 Agent “思考”和调用工具时,可以先给用户返回一个“正在思考中...”或“正在为您查询...”的占位信息。对于 LLM 生成文本的部分,务必开启流式输出,让用户看到文字逐个出现,而不是长时间白屏。这能极大提升感知速度。
- 并行与异步:仔细分析你的工作流。有些步骤可以并行吗?例如,在“需求澄清”阶段,如果需要查询多个不相关的技术组件的知识,可以并行发起多个工具调用。使用
asyncio.gather能显著缩短整体耗时。 - LLM 调用优化:
- 温度(Temperature)设置:在需要创造性、发散性思维的“头脑风暴”阶段,可以设高一点(如 0.8)。在需要稳定、确定性输出的“工具调用决策”或“结构化输出”阶段,一定要调低(如 0.1 或 0),避免它“胡言乱语”导致流程出错。
- 模型选型:不是所有步骤都需要最强大、最贵的模型。对于“对话摘要”、“意图分类”这类相对简单的任务,可以使用更小、更快的模型(如 Claude Haiku, GPT-3.5-turbo),把 GPT-4 这样的“重型武器”用在最核心的规划和复杂推理上。这能有效降低成本并提升速度。
4.3 可观测性与调试:给 Agent 装上“黑匣子”
Agent 内部决策过程不透明,出了问题很难排查。必须建立完善的可观测性体系。
全链路日志:记录下每一轮的关键信息。我建议的结构化日志字段包括:
session_idturn_number(第几轮对话)user_inputllm_prompt_snapshot(发送给 LLM 的完整提示词,脱敏后)llm_full_response(LLM 的完整回复)parsed_action(解析出的行动,如query_arch_knowledge)tool_input_output(工具的输入和输出)current_state(执行后的状态)final_response_to_user(最终给用户的回复) 将这些日志输出到 ELK(Elasticsearch, Logstash, Kibana)或 Grafana Loki,便于搜索和聚合分析。
构建调试界面:开发一个内部管理界面,可以输入任意
session_id,回放整个对话流程,查看每一轮 LLM 的“思考过程”、工具调用的详情和状态变迁。这是定位“为什么 Agent 在这里卡住了”或“为什么它做出了这个奇怪决定”的终极武器。关键指标监控:
- 业务指标:会话成功率、平均完成轮次、用户主动中断率、架构图生成率。
- 性能指标:每轮平均响应时间、LLM API 调用耗时分布、工具调用耗时。
- 成本指标:各模型 Token 消耗量、每日/每月成本趋势。 设置告警,当会话成功率骤降或平均响应时间异常拉长时,能第一时间收到通知。
5. 避坑指南与常见问题排查
在实际开发和上线后,我遇到了不少典型问题。这里列几个最有代表性的,希望能帮你绕过这些坑。
5.1 Agent 陷入循环或偏离主题
现象:Agent 反复问同一个问题,或者在某个细节上钻牛角尖,无法推进到下一阶段。根因分析:
- 状态机设计有漏洞:某个状态缺少出口条件,或者状态转移逻辑有误。
- LLM 的提示词引导力不足:系统提示词中没有强调“推进任务”的目标,或者对“完成条件”描述模糊。
- 工具返回结果质量差:工具返回的信息无法帮助 LLM 做出有效决策,导致它“不知所措”,只能原地打转。
解决方案:
- 强化状态机:为每个状态设置超时或最大重试次数。例如,在“需求澄清”状态,如果连续3轮对话都在问类似问题而没有收集到新信息,则强制跳转到“方案建议”状态,并基于已有(可能不完整的)信息进行。
- 优化提示词:在系统提示词中明确加入“任务推进指令”,例如:“你的首要目标是高效地收集齐所有必要信息,并引导对话走向生成架构图。避免在无关紧要的细节上过度纠缠。”
- 改进工具:确保知识检索工具返回的信息是简洁、相关、权威的。如果工具频繁返回“未找到”,考虑扩充知识库或改进检索算法。
5.2 生成的架构图逻辑混乱或不符合常识
现象:最终生成的图里,组件关系奇怪,比如“数据库直接调用前端”。根因分析:
- LLM 的领域知识不足或幻觉:LLM 可能对某些专业的架构原则理解有偏差。
- 结构化输出约束不够强:虽然要求输出 JSON,但 JSON 中的
connections字段可能包含了不合逻辑的连接。 - 缺乏事后验证:生成 JSON 后,没有经过任何规则校验就直接绘图。
解决方案:
- 知识增强:在知识库中不仅存放组件介绍,还要存放架构约束规则。例如,可以有一个规则库:“
external类型的组件不能直接连接database类型的组件”。在 LLM 规划或最终输出前,将这些规则作为上下文喂给它。 - 输出后校验:在将 JSON 送入渲染引擎前,增加一个轻量级规则校验层。用一组简单的 if-else 规则或一个小的分类模型,检查
connections的合理性,过滤掉明显违背常识的连接。 - 提供修正机会:将生成的架构图描述(文本形式)先展示给用户确认,问一句“这是您想要的架构吗?有没有需要调整的地方?”让用户参与最终把关,这比直接给一张可能有错的图要好。
5.3 处理用户模糊、多变或错误的需求
现象:用户一开始说“要微服务”,后来又说“好像单体也行”;或者用户提出的技术组合本身存在冲突。根因分析:Agent 被设计成“顺从”和“帮助”用户,但当用户需求本身矛盾或低效时,它可能无法做出专业反驳。
解决方案:
- 构建“挑战”能力:在工具集中增加一个
challenge_assumption工具。当 LLM 检测到用户需求可能存在矛盾(如“既要强一致性又要无限水平扩展”)或明显反模式(如“所有服务都直连数据库主库”)时,可以调用此工具。该工具会基于知识库,生成一段友好的、专业的提醒文本,例如:“注意到您提到了‘强一致性’和‘无限扩展’。根据常见的分布式系统理论,CAP 定理指出三者难以兼得。我们通常需要在一致性和扩展性之间做出权衡。您更看重哪一方面?” - 记录需求变更历史:在会话状态中,不仅记录当前确认的需求,也记录需求变更的轨迹。当用户频繁变更核心需求时,Agent 可以主动总结:“我看到您对架构风格的想法从微服务调整到了单体。为了确保我们方向一致,我是否可以确认,我们最终确定采用单体架构作为讨论基础?” 这能帮助锁定范围,避免无效讨论。
5.4 成本失控与响应延迟
现象:随着用户增多,API 调用费用暴涨,且响应速度越来越慢。根因分析:
- 上下文无限增长:没有做摘要和压缩,每次对话都携带全部历史,Token 消耗量呈线性甚至指数增长。
- 工具调用冗余:相同的问题被反复查询,没有缓存。
- 模型滥用:所有步骤都用最贵的模型。
解决方案(结合4.2节的优化):
- 实施严格的上下文窗口管理:如前所述,采用分层记忆策略,这是控制成本最有效的手段。
- 为工具调用添加缓存:特别是对于知识查询类工具。为查询结果建立缓存(如使用 Redis,设置合理的 TTL)。相同的查询命中缓存后,可以立即返回,速度极快且成本为零。
- 实施速率限制和负载均衡:在 Agent 服务层,对每个用户或每个 API Key 实施速率限制(如每分钟最多发起 10 轮对话)。同时,确保你的后端渲染服务(如 PlantUML 服务器)有足够的实例来处理并发绘图请求,避免这里成为瓶颈。
设计并实现一个能进行多轮对话并生成架构图的 Agent,是一个典型的系统工程,它考验的不仅仅是对大语言模型 API 的调用,更是对复杂工作流、状态管理和人机交互设计的综合能力。从简单的提示词工程,到复杂的工具编排和状态维护,每一步都需要在灵活性和可控性之间找到平衡。我个人的体会是,启动一个可用的原型很快,但打磨一个稳定、可靠、用户体验良好的生产级系统,需要持续地迭代、观察和优化。最宝贵的经验往往来自于真实的用户对话日志,多看看用户在哪里困惑、在哪里放弃,那些地方就是你下一个需要重点改进的“价值洼地”。