package com.xly.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.agent.AgentIdentity; import com.xly.agent.EventLogChatMemory; import com.xly.agent.ReActAgent; import com.xly.service.ErpClient; import com.xly.service.EventProjectionService; import com.xly.service.FormRenderService; import com.xly.service.FormResolverService; import com.xly.service.LedgerService; import com.xly.service.OpService; import com.xly.service.PreviewService; import com.xly.service.SkillService; import com.xly.service.SystemPromptService; import com.xly.tool.ErpReadTool; import com.xly.tool.FormCollectTool; import com.xly.tool.InteractionTool; import com.xly.tool.KgQueryTool; import com.xly.tool.PreviewChangeTool; import com.xly.tool.UseSkillTool; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.service.AiServices; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; /** * 按「每次请求的身份」组装单一 ReAct agent(架构 §5/§7 的 per-call context)。 * *
为什么按请求新建:LangChain4j 流式工具执行发生在模型 HTTP 回调线程,而非控制器工作线程, * ThreadLocal 不可靠。于是把 {@link AgentIdentity}(透传 token + 权限集)直接注入到**每请求新建**的 * 工具实例里(ErpReadTool/PreviewChangeTool/FormCollectTool),从根上保证鉴权与 token 正确。 * *
固定 7 工具:useSkill/findForms/readFormData/lookupRecord/collectForm/previewChange/askUser。 * **模型可用的工具全部只读/只渲染**(rearch3 §1/§3):写路径 = 预览卡/表单 → 用户点按钮 → * 确定性端点写 ai_op_queue,模型碰不到任何写入口。 * 无状态、全局的工具({@link KgQueryTool}、{@link InteractionTool})是单例、跨请求复用。 */ @Component public class AgentFactory { private final StreamingChatModel streamingModel; private final LedgerService ledger; private final EventProjectionService projection; private final SystemPromptService systemPromptService; private final ErpClient erp; private final FormResolverService resolver; private final FormRenderService render; private final PreviewService previews; private final ObjectMapper mapper; private final KgQueryTool kgQueryTool; private final InteractionTool interactionTool; private final SkillService skillService; public AgentFactory(@Qualifier("agentStreamingModel") StreamingChatModel streamingModel, LedgerService ledger, EventProjectionService projection, SystemPromptService systemPromptService, ErpClient erp, FormResolverService resolver, FormRenderService render, PreviewService previews, OpService ops, ObjectMapper mapper, KgQueryTool kgQueryTool, InteractionTool interactionTool, SkillService skillService) { this.streamingModel = streamingModel; this.ledger = ledger; this.projection = projection; this.systemPromptService = systemPromptService; this.erp = erp; this.resolver = resolver; this.render = render; this.previews = previews; this.mapper = mapper; this.kgQueryTool = kgQueryTool; this.interactionTool = interactionTool; this.skillService = skillService; // 流程卡的「已提交待办」行读 ai_op_queue.sStatus 只读进度(rearch3 §3) projection.setOpStatusLookup(ops::statusLabel); } /** * 组装 ReAct agent:固定 7 工具 + 唯一 system prompt。 * {@code maxSequentialToolsInvocations} 作为循环护栏,杜绝失控 ReAct 循环。 * * @param convId 会话 id(useSkill/previewChange 落事件需要) * @param internalUserTurn true=本次的用户消息是系统注入话术(护栏重试),落账时带 internal 标记, * 前端历史不显示 */ public ReActAgent build(AgentIdentity identity, String convId, boolean internalUserTurn) { Object[] tools = new Object[]{kgQueryTool, interactionTool, new UseSkillTool(skillService, ledger, convId), new ErpReadTool(erp, resolver, identity), new PreviewChangeTool(previews, identity, convId), new FormCollectTool(render, resolver, identity, mapper)}; return AiServices.builder(ReActAgent.class) .streamingChatModel(streamingModel) .tools(tools) .maxSequentialToolsInvocations(8) // 循环护栏:防止 askUser/工具无限自我循环 // 事件账本记忆:逐条 append 原子落账,读取为四段式 token 记账投影(见 EventLogChatMemory) .chatMemoryProvider(memoryId -> new EventLogChatMemory( String.valueOf(memoryId), ledger, projection, identity, internalUserTurn)) .systemMessageProvider(memoryId -> systemPromptService.prompt()) .build(); } }