【学习笔记】Unity C# 代码审查 Agent 开发记录:第四阶段(Memory、流式输出与部署)

前言

按照个人学习计划,第四阶段(原计划 4.20-4.26)的任务是让 Agent 具备多轮对话记忆、流式输出,并部署到公网。实际执行中因事推迟了几天,于 4.29-5.4 完成。本文记录这一阶段的实现过程、代码要点及踩坑记录。

实际完成时间线

原计划日期 任务 实际完成日期
4.20 LangChain Memory 学习 4.29
4.21 对话历史管理接口 5.2
4.22 前端聊天界面 5.2
4.23 流式输出 Agent 思考过程 5.4
4.24 部署后端到 Railway 5.4
4.25 部署前端到 Netlify 5.4

一、LangChain Memory 与多轮对话

要让 Agent 支持多轮对话,需要将之前的对话历史保存下来,并在每次请求时一并发送给 LLM。

1. 数据模型

在 models.py 中新增请求模型,添加 session_id 字段用于区分不同会话:

class AgentMemoryRequest(BaseModel):
    input: str = Field(..., description="用户输入")
    session_id: str = Field(default="default", description="会话ID")

2. 会话存储

在 agent_api.py 中,使用字典管理多用户会话历史:

session_store: Dict[str, list] = {}

def get_session_history(session_id: str) -> list:
    if session_id not in session_store:
        session_store[session_id] = []
    return session_store[session_id]

def add_message_to_session(session_id: str, message):
    session_store.setdefault(session_id, []).append(message)

3. Memory 接口

新建 /agent/memory 接口,将历史消息拼接到当前消息前发送给 LLM,并在收到回复后保存本轮对话:

@app.post("/agent/memory")
async def agent_with_memory(request: AgentMemoryRequest):
    history = get_session_history(request.session_id)
    # 拦截逻辑:只有全新会话且不含代码关键词时才拦截
    if not history and not is_code_review_intent(request.input):
        return UnifiedResponse(success=True, code=200, message="非代码请求已拦截", ...)

    messages = [SystemMessage(content="你是专业的 Unity C# 代码审查助手...")]
    messages.extend(history)
    messages.append(HumanMessage(content=request.input))

    result = await agent_graph.ainvoke({"messages": messages})

    add_message_to_session(request.session_id, HumanMessage(content=request.input))
    add_message_to_session(request.session_id, result["messages"][-1])

    return UnifiedResponse(success=True, data={"output": result["messages"][-1].content})

测试:先用 "public int age;" 开启会话,再问 "刚才那个字段有什么问题?",Agent 能准确复述之前的审查内容。


二、对话历史管理接口

为了方便管理,增加了查看和清除历史的接口:

  • GET /agent/memory/history?session_id=xxx:返回指定会话的历史记录
  • DELETE /agent/memory/history?session_id=xxx:清除指定会话历史

实现中需要注意:GET 和 DELETE 没有请求体,参数通过 Query 传递。例如:

@app.get("/agent/memory/history")
async def get_memory_history(session_id: str = Query(..., description="会话ID")):
    history = get_session_history(session_id)
    # ... 构造返回数据

三、流式输出(SSE)

问题ainvoke 需要等待 Agent 完全执行完毕才返回,用户体验不佳。希望实现类似 ChatGPT 的打字机效果。

方案:使用 LangGraph 的 astream_events 方法,并通过 FastAPI 的 StreamingResponse 实现 SSE。

后端

@app.post("/agent/memory/stream")
async def agent_with_memory_stream(request: AgentMemoryRequest):
    # ... 准备 messages ...
    async def stream_events():
        async for event in agent_graph.astream_events({"messages": messages}, version="v2"):
            kind = event.get("event")
            if kind == "on_chat_model_stream":
                chunk = event["data"]["chunk"]
                yield f"data:{json.dumps({'type':'text','content':chunk.content})}\n\n"
            elif kind == "on_tool_start":
                yield f"data: {json.dumps({'type':'tool_start','content':f'调用工具: {event["name"]}...'})}\n\n"
            # ... 其他事件处理 ...
    return StreamingResponse(stream_events(), media_type="text/event-stream")

前端:使用 fetch + ReadableStream 逐块解析 SSE 数据,动态更新消息气泡。

const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    // 解析 data: 行,更新 UI
}

四、前后端部署

后端 Railway

  1. 编写 requirements.txt 和 runtime.txt(指定 Python 3.12)
  2. 推送代码到 GitHub
  3. 在 Railway 新建 Web Service,设置 Start Command uvicorn main:app --host 0.0.0.0 --port 10000
  4. 添加环境变量 DEEPSEEK_API_KEY
  5. 获得公网地址

前端 Netlify

  1. 修改 index.html 中 API_BASE 为 Railway 地址
  2. 将项目导入 Netlify,自动部署
  3. 注意:runtime.txt 会使 Netlify 误判为 Python 项目,需加入 .gitignore

五、小结

这一阶段完成了:

  • ✅ Memory 多轮对话
  • ✅ 历史管理接口
  • ✅ 前端聊天界面
  • ✅ 流式输出
  • ✅ 后端 Railway + 前端 Netlify 部署

项目已可公网访问,具备实际演示能力。后续将继续优化提示词和管理 Token 消耗。

更多推荐