package com.xly.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.agent.AgentIdentity; import com.xly.agent.ReActAgent; import com.xly.service.AuditService; import com.xly.service.ErpClient; import com.xly.service.FormResolverService; import com.xly.service.OpService; 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.ProposeWriteTool; import com.xly.tool.QueryTool; import com.xly.tool.SkillTool; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.service.AiServices; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; /** * 按「每次请求的身份」组装单一 ReAct agent(架构 §5/§7 的 per-call context)。 * *

为什么按请求新建:LangChain4j 流式工具执行发生在模型 HTTP 回调线程,而非控制器工作线程, * ThreadLocal 不可靠。于是把 {@link AgentIdentity}(透传 token + 权限集)直接注入到**每请求新建**的 * 工具实例里(ErpReadTool/ProposeWriteTool/QueryTool/FormCollectTool),从根上保证鉴权与 token 正确。 * *

无状态、全局的工具({@link KgQueryTool} 表单目录/KG、{@link SkillTool} Skill 加载、 * {@link InteractionTool} AskUser)是单例、跨请求复用。模型、记忆存储、system prompt 也复用。 */ @Component public class AgentFactory { private final OllamaStreamingChatModel streamingModel; private final OllamaChatModel sqlModel; private final RedisChatMemoryStore memoryStore; private final SystemPromptService systemPromptService; private final ErpClient erp; private final JdbcTemplate jdbc; private final FormResolverService resolver; private final OpService ops; private final ObjectMapper mapper; private final AuditService audit; private final SkillService skillService; private final KgQueryTool kgQueryTool; private final SkillTool skillTool; private final InteractionTool interactionTool; private volatile String cachedSystemPrompt; public AgentFactory(@Qualifier("agentStreamingModel") OllamaStreamingChatModel streamingModel, @Qualifier("sqlChatModel") OllamaChatModel sqlModel, RedisChatMemoryStore memoryStore, SystemPromptService systemPromptService, ErpClient erp, JdbcTemplate jdbc, FormResolverService resolver, OpService ops, ObjectMapper mapper, AuditService audit, SkillService skillService, KgQueryTool kgQueryTool, SkillTool skillTool, InteractionTool interactionTool) { this.streamingModel = streamingModel; this.sqlModel = sqlModel; this.memoryStore = memoryStore; this.systemPromptService = systemPromptService; this.erp = erp; this.jdbc = jdbc; this.resolver = resolver; this.ops = ops; this.mapper = mapper; this.audit = audit; this.skillService = skillService; this.kgQueryTool = kgQueryTool; this.skillTool = skillTool; this.interactionTool = interactionTool; } /** system prompt 是全局的(L1 域图 + Skill 摘要),构建一次后缓存。 */ private String systemPrompt() { String p = cachedSystemPrompt; if (p == null) { synchronized (this) { if (cachedSystemPrompt == null) { cachedSystemPrompt = systemPromptService.buildSystemPrompt(); } p = cachedSystemPrompt; } } return p; } /** 用给定身份组装一个 ReAct agent(工具实例携带该身份的 token 与权限边界)。 */ public ReActAgent build(AgentIdentity identity) { String systemPrompt = systemPrompt(); return AiServices.builder(ReActAgent.class) .streamingChatModel(streamingModel) .tools( kgQueryTool, skillTool, interactionTool, new ErpReadTool(erp, jdbc, resolver, identity), new ProposeWriteTool(erp, jdbc, ops, mapper, identity), new QueryTool(sqlModel, jdbc, audit, identity), new FormCollectTool(erp, jdbc, resolver, identity, mapper)) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(30) .chatMemoryStore(memoryStore) .build()) .systemMessageProvider(memoryId -> systemPrompt) .build(); } }