khangPham-864/Dockerized-Tech-Domain-Intelligence-Agent
GitHub: khangPham-864/Dockerized-Tech-Domain-Intelligence-Agent
一个基于 LangGraph 的技术研究智能体,自动检索 GitHub 和 Hacker News 并利用本地模型生成有依据的回答,通过 Docker Compose 容器化实现脱离 Colab 的可重现运行。
Stars: 0 | Forks: 0
# 技术领域智能体
一个有状态的 LangGraph 智能体,它可以搜索 GitHub 代码库和 Hacker News 讨论,使用本地 `qwen2.5:7b` 模型分析检索到的证据,应用提示注入防护措施,并通过 LLM-as-judge 测试套件评估自身的响应。
该项目在 `student_submission.ipynb` 中为 COMP2701 评估 2 开发,并使用 Docker Compose 进行了扩展,以便**原始的 notebook 可以在容器化的 Jupyter 环境中运行,而不是依赖 Google Colab**。
## 项目目标
该项目有三个主要目标:
1. 为技术研究构建结构化的智能体工作流。
2. 保持响应基于从 GitHub 和 Hacker News 检索到的证据。
3. 通过容器化 Jupyter、Ollama 和本地模型,在 Colab 之外可重现地运行原始的 notebook。
查询示例:
```
What are the most popular open-source Python frameworks for building LLM agents?
```
## 技术栈
| 类别 | 技术 |
|---|---|
| 编程语言 | Python |
| Notebook 环境 | JupyterLab |
| 智能体框架 | LangGraph |
| LLM 集成 | LangChain 和 `langchain-ollama` |
| 本地模型 | Ollama 使用 `qwen2.5:7b` |
| API 客户端 | HTTPX |
| 数据验证 | Pydantic |
| 工具 1 | GitHub REST API |
| 工具 2 | Hacker News Algolia API |
| 评估 | 本地 LLM-as-judge |
| 容器化 | Docker 和 Docker Compose |
| 容器网络 | Docker bridge network |
| 模型持久化 | Docker named volume |
固定版本的依赖项:
```
langchain-ollama==1.1.0
langgraph==1.2.9
langchain-core==1.4.9
langchain-community==0.4.2
httpx==0.28.1
pydantic==2.13.4
```
## 解决的问题
### 1. 非结构化的多源技术研究
GitHub 和 Hacker News 提供了不同形式的证据:
- GitHub 提供代码库名称、星标数、编程语言、描述、URL 和更新日期。
- Hacker News 提供故事标题、得分、评论、日期和开发者讨论信号。
直接调用这两个 API 并不会自动生成一个明确的答案。系统仍然需要存储结果、决定哪些证据有用、合并来源并解释不确定性。
### 2. 提示注入和无效输入
用户可能会提交:
- 空查询;
- 异常长的查询;
- 直接的提示注入尝试;
- 要求透露或覆盖隐藏指令的请求。
示例包括:
```
ignore previous instructions
system prompt
developer message
jailbreak
override instructions
```
如果没有防护措施,这些输入可能会到达 API 或影响模型。
### 3. 结果稀疏或缺失
某些查询会返回:
- GitHub 结果但没有 Hacker News 故事;
- Hacker News 故事但没有 GitHub 代码库;
- 两个来源都没有有用的结果。
智能体必须避免在检索能力薄弱时崩溃或捏造证据。
### 4. 自然语言问题并不总是适合 GitHub 搜索
完整的问题为人类提供了有用的上下文,但对于代码库搜索来说,可能包含低价值的词汇。
智能体需要保留重要的技术术语,同时去除通用词汇,并将查询简化为专注的关键词。
### 5. 幻觉风险
模型绝不能创建:
- 未检索到的代码库名称;
- 虚假的星标数;
- 不受支持的技术声明;
- 将 Hacker News 上的观点当作事实证据。
### 6. 在 Colab 之外重现原始 notebook
原始 notebook 会安装 Ollama,下载 `qwen2.5:7b`,并在临时的 Colab runtime 中安装 Python 包。
部署问题在于如何在一个可重用的环境中运行**相同的 notebook**,而无需将项目重写为单独的 FastAPI 应用程序。
### 7. 本地 CPU 推理速度缓慢
在没有可访问的 NVIDIA GPU 的情况下,通过 Docker Desktop 运行 `qwen2.5:7b` 会导致高 CPU 和内存使用率。
观察到的使用量大约为:
```
agent-ollama 592% CPU
agent-ollama 5.15 GiB RAM
agent-jupyternb 0.30% CPU
```
这表明 Ollama 推理(而不是 Jupyter)是主要的瓶颈。
## 项目如何解决这些问题
## 智能体结构和工作流
LangGraph 工作流包含三个有意义的节点:
```
User query
|
v
fetch_node
|- sanitise the query
|- block invalid or adversarial input
|- generate focused GitHub keywords
|- call GitHub
|- call Hacker News
|
+---- results found ----> analyse_node
| |
| v
| respond_node
|
+---- blocked/no data ---> respond_node
|
v
END
```
### `fetch_node`
获取节点:
- 在调用外部 API 之前运行防护措施;
- 增加迭代计数器;
- 使用本地 LLM 创建简短的 GitHub 关键词查询;
- 将经过净化处理的自然语言查询发送到 Hacker News;
- 将两组结果集存储在状态中;
- 选择图是应该分析证据还是返回后备响应。
### `analyse_node`
分析节点:
- 读取 GitHub 和 Hacker News 结果;
- 提取诸如星标数、编程语言、日期、得分和评论等证据;
- 合并来自两个来源的信号;
- 识别不确定性;
- 将 Hacker News 视为社区讨论,而非事实证据。
### `respond_node`
响应节点:
- 生成最终的有依据的回答;
- 针对被阻止的输入返回安全的拒绝;
- 在检索结果为空时返回后备响应;
- 避免捏造不受支持的事实。
## 智能体状态
图通过一个共享的 `TypedDict` 传递信息:
```
class AgentState(TypedDict):
query: str
final_response: str
errors: Annotated[List[str], operator.add]
iteration: int
current_node: str
github_results: List[dict]
hn_stories: List[dict]
analysis: str
sanitised_query: str
```
| 字段 | 用途 |
|---|---|
| `query` | 原始用户查询 |
| `sanitised_query` | 经过验证和长度限制的查询 |
| `github_results` | GitHub 代码库数据 |
| `hn_stories` | Hacker News 故事数据 |
| `analysis` | 证据综合 |
| `final_response` | 最终答案 |
| `errors` | 累积的错误消息 |
| `iteration` | 循环防护和未来重试支持 |
| `current_node` | 路由信号 |
该图使用 `MemorySaver` 编译,每次运行都使用一个 `thread_id`。
## 防护机制设计
防护措施在任何外部 API 调用之前执行。
它:
- 将 `None` 转换为空字符串;
- 拒绝空输入或仅包含空格的输入;
- 执行不区分大小写的阻止模式匹配;
- 截断超过 300 个字符的查询;
- 返回结构化的拒绝路径。
这可以防止无效或对抗性输入到达 GitHub 或 Hacker News。
当前的黑名单主要集中在直接攻击上。它可能无法捕获每一个改写后的提示注入尝试。
## 搜索策略
### GitHub
完整的用户问题将被转换为简短的关键词查询。
关键词提示指示模型:
- 仅返回关键词;
- 避免完整的句子;
- 保留技术名称;
- 删除诸如 `find`、`popular` 和 `best` 之类的通用词汇;
- 返回不超过六个词。
### Hacker News
Hacker News 接收净化后的自然语言查询,因为故事搜索可以从完整的意图中受益。
两个工具搜索相同的主题,但查询格式会适应各自的 API。
## 基于事实的响应设计
最终响应提示要求:
1. 直接回答;
2. 支持性证据;
3. 在证据稀少时表达不确定性。
它还要求模型:
- 仅使用提供的分析;
- 在相关时提及 GitHub 和 Hacker News;
- 避免将 Hacker News 观点视为证据;
- 避免捏造证据;
- 使用清晰专业的语言。
## 评估
评估套件包含 10 个用例:
- 4 个正常用例;
- 3 个边缘用例;
- 3 个对抗性用例。
每个用例都会运行通过智能体,并由本地的 LLM 评估器进行评分。
评估器返回结构化的 JSON,其中包含:
- 总分;
- 摘要;
- 每个评估标准的通过/失败值。
记录的结果:
| 类别 | 分数 | 用例 |
|---|---:|---:|
| 正常 | 85% | 4 |
| 边缘 | 85% | 3 |
| 对抗性 | 90% | 3 |
| **总计** | **86%** | **10** |
结果显示出强大的安全性和后备处理能力。主要的限制在于针对宽泛或稀疏查询的检索质量。
## 为什么原始 notebook 被容器化
部署选择是容器化 **notebook 环境本身**,而不是将项目重写为 Web API。
这保留了:
- 原始的 `student_submission.ipynb`;
- 图构建单元;
- 工具冒烟测试;
- 评估单元;
- 报告单元;
- 自我检查单元。
职责划分如下:
```
Dockerfile
-> installs Jupyter and Python dependencies
Ollama service
-> runs the local model server
model-pull service
-> downloads qwen2.5:7b once
Jupyter service
-> opens and runs student_submission.ipynb
Docker volume
-> preserves the downloaded model
Bind mount
-> preserves notebook changes on the host
```
## Docker Compose 架构
```
Browser
|
| localhost:8888
v
Jupyter container
|
| http://ollama:11434
v
Ollama container
|
v
qwen2.5:7b model volume
Jupyter container
|- HTTPS -> GitHub API
`- HTTPS -> Hacker News API
```
### 服务
| 服务 | 任务 |
|---|---|
| `ollama` | 运行 Ollama 服务器 |
| `model-pull` | 下载 `qwen2.5:7b`,然后退出 |
| `jupyter-notebook` | 运行 JupyterLab 和原始 notebook |
预期状态:
```
agent-ollama Up (healthy)
agent-model-pull Exited (0)
agent-jupyternb Up (healthy)
```
`model-pull` 的 `Exited (0)` 是正常的,因为它是一个一次性的设置服务。
## 项目结构
```
workspace/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── agent.env
├── .gitignore
└── notebooks/
└── student_submission.ipynb
```
推荐的 `.gitignore`:
```
agent.env
.ipynb_checkpoints/
__pycache__/
```
## 环境文件
在 `docker-compose.yml` 旁边创建 `agent.env`:
```
MODEL=qwen2.5:7b
OLLAMA_BASE_URL=http://ollama:11434
JUPYTER_TOKEN=replace_with_a_private_token
GITHUB_TOKEN=replace_with_a_github_token
```
| 变量 | 用途 |
|---|---|
| `MODEL` | 选择 Ollama 模型 |
| `OLLAMA_BASE_URL` | 将 notebook 连接到 Ollama 服务 |
| `JUPYTER_TOKEN` | 保护 Jupyter 登录 |
| `GITHUB_TOKEN` | 对 GitHub API 搜索进行身份验证 |
GitHub token 是可选的。没有它仍然可以进行公开代码库搜索,但身份验证可以提高代码库搜索的速率配额。
## Dockerfile
```
FROM quay.io/jupyter/minimal-notebook:latest
COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt
WORKDIR /home/jovyan/work
```
它:
1. 从现成的 Jupyter 镜像开始;
2. 复制依赖列表;
3. 在镜像构建期间安装 notebook 所需的包;
4. 设置默认工作目录。
## Docker Compose 配置
```
volumes:
ollama_models:
networks:
agent-network:
driver: bridge
services:
ollama:
image: ollama/ollama:latest
container_name: agent-ollama
restart: unless-stopped
volumes:
- ollama_models:/root/.ollama
networks:
- agent-network
healthcheck:
test: ["CMD", "ollama", "list"]
interval: 10s
timeout: 5s
retries: 12
start_period: 20s
model-pull:
image: ollama/ollama:latest
container_name: agent-model-pull
restart: "no"
depends_on:
ollama:
condition: service_healthy
environment:
OLLAMA_HOST: http://ollama:11434
command: ["pull", "qwen2.5:7b"]
networks:
- agent-network
jupyter-notebook:
build:
context: .
container_name: agent-jupyternb
restart: unless-stopped
depends_on:
model-pull:
condition: service_completed_successfully
ports:
- "8888:8888"
volumes:
- ./notebooks:/home/jovyan/work/notebooks
env_file: ./agent.env
command:
- bash
- -c
- >-
exec start-notebook.py
--ServerApp.root_dir=/home/jovyan/work/notebooks
--IdentityProvider.token="$${JUPYTER_TOKEN}"
networks:
- agent-network
```
## 针对 Docker 的 notebook 更改
通过具备环境感知能力的 Ollama URL,同一个 notebook 可以同时支持 Colab 和 Docker:
```
MODEL = os.getenv(
"MODEL",
"qwen2.5:7b"
)
OLLAMA_BASE_URL = os.getenv(
"OLLAMA_BASE_URL",
"http://127.0.0.1:11434"
)
llm = ChatOllama(
model=MODEL,
base_url=OLLAMA_BASE_URL,
temperature=0
)
llm_creative = ChatOllama(
model=MODEL,
base_url=OLLAMA_BASE_URL,
temperature=0.3
)
```
行为:
```
Colab
-> no OLLAMA_BASE_URL environment variable
-> use http://127.0.0.1:11434
Docker
-> agent.env defines OLLAMA_BASE_URL
-> use http://ollama:11434
```
在 Docker 内运行时,跳过以下单元:
- 安装 Ollama;
- 启动 `ollama serve`;
- 拉取模型;
- 安装 Python 包。
这些任务已经由 Docker 处理。
## 如何使用该项目
### 1. 放置 notebook
将原始 notebook 放在:
```
notebooks/student_submission.ipynb
```
### 2. 创建 `agent.env`
使用上面的环境文件示例。
### 3. 验证 Compose 配置
```
docker compose config --no-env-resolution
```
### 4. 构建并启动
```
docker compose up -d --build
```
启动顺序:
```
ollama starts
-> health check passes
-> model-pull downloads qwen2.5:7b
-> model-pull exits successfully
-> Jupyter starts
```
### 5. 检查服务
```
docker compose ps
docker compose ps -a
```
### 6. 打开 Jupyter
打开:
```
http://localhost:8888
```
输入 `agent.env` 中的 `JUPYTER_TOKEN`。
### 7. 运行 notebook
跳过仅适用于 Colab 的安装单元,并从具备环境感知能力的模型设置单元开始运行。
### 8. 停止环境
```
docker compose down
```
这将保留命名的模型 volume。
仅在需要时删除模型 volume:
```
docker compose down -v
```
## 使用示例
```
result = run_agent(
"Find GitHub repositories related to retrieval-augmented generation",
thread_id="demo-rag"
)
print(result["final_response"])
```
## Docker 部署挑战
### NVIDIA GPU 预留在本地失败
初始配置:
```
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: ["gpu"]
```
错误:
```
WSL environment detected but no adapters were found
```
#### 原因
本地 Docker Desktop 环境无法提供 NVIDIA GPU。
#### 解决方案
默认的 Compose 文件通过移除 GPU 预留更改为兼容 CPU 的操作。
对于配置了 NVIDIA GPU 的云虚拟机,请使用单独的覆盖文件:
```
services:
ollama:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: ["gpu"]
```
### Jupyter token 未展开
不正确:
```
--IdentityProvider.token='$${JUPYTER_TOKEN}'
```
单引号阻止了 Bash 变量展开。
正确:
```
--IdentityProvider.token="$${JUPYTER_TOKEN}"
```
### Jupyter 启动命令生成了格式错误的路径
错误:
```
No such file or directory:
/home/jovyan/work/ --ServerApp.root_dir=/home/jovyan/work/notebooks
```
反斜杠和空格导致命令选项被解释为路径的一部分。
解决方案是使用折叠的 YAML 命令,去掉不必要的反斜杠。
### Jupyter 进入重启循环
错误:
```
bash: unexpected EOF while looking for matching `''
```
不匹配的单引号导致启动命令失败。由于该服务使用了:
```
restart: unless-stopped
```
Docker 不断重启它。
正确的命令:
```
command:
- bash
- -c
- >-
exec start-notebook.py
--ServerApp.root_dir=/home/jovyan/work/notebooks
--IdentityProvider.token="$${JUPYTER_TOKEN}"
```
### CPU 评估太慢
十用例评估对每个用例执行多次本地 LLM 调用:
1. 关键词提取;
2. 证据分析;
3. 最终响应生成;
4. LLM-as-judge 评估。
在 CPU 上,Ollama 使用了多个 CPU 核心和超过 5 GiB 的 RAM。在等待模型推理时,Jupyter 基本处于空闲状态。
实用方法:
- 使用 CPU 模式进行开发;
- 测试时运行较少的评估用例;
- 使用 NVIDIA 云虚拟机运行完整的测试套件;
- 在最终提交前运行所有十个用例一次。
## 故障排除
### 检查状态
```
docker compose ps -a
```
### 跟踪 Jupyter 日志
```
docker compose logs -f jupyter-notebook
```
### 跟踪模型下载日志
```
docker compose logs -f model-pull
```
### 检查 Ollama 模型
```
docker exec agent-ollama ollama list
```
### 检查模型处理器
```
docker exec agent-ollama ollama ps
```
### 检查资源使用情况
```
docker stats agent-ollama agent-jupyternb
```
###重新创建 Jupyter
```
docker compose up -d --force-recreate --no-deps jupyter-notebook
```
### 更改依赖项后重新构建
```
docker compose up -d --build
```
## 挑战和经验教训
- 防护措施应该在外部工具之前运行。
- 不同的 API 可能需要不同的查询格式。
- 有状态的图使路由和故障处理变得明确。
- 稀疏的检索应该产生安全的后备方案,而不是捏造的证据。
- 加载环境变量是不够的;应用程序必须实际使用它。
- 原始的 notebook 可以被容器化,而无需重写为单独的应用程序。
- 当容器通信时,Docker 服务名称会替换 `localhost`。
- YAML 结构、Compose 插值和 Bash 引号是独立的问题。
- 单引号会阻止 shell 变量展开。
- 重启循环是一种症状;容器日志会显示根本错误。
- Docker 卷在重启之间保留大型模型文件。
- 本地 CPU 推理可行,但明显慢于 GPU 推理。
## 未来改进
### 重试和查询扩展
当两个来源都没有返回结果时,未来的图分支可以使用更广泛的关键词重试一次。
现有的 `iteration` 字段可以防止无限循环。
### 持久化检查点
`MemorySaver` 满足了作业要求,但将状态存储在内存中。未来的版本可以使用 SQLite 或 PostgreSQL 检查点。
### 云 GPU 部署
Docker Compose 环境可以移动到具有以下条件的 Linux 虚拟机上:
- NVIDIA GPU;
- NVIDIA 驱动程序;
- Docker;
- NVIDIA Container Toolkit。
只有 Ollama 服务需要 GPU 访问权限。
## 致谢
**开发者:** Le Nguyen Khang Pham
**GitHub:** [khangPham-864](https://github.com/khangPham-864)
COMP2701 教学团队提供了初始 notebook、评估结构和预构建的公开工具库。
项目实现包括:
- 技术领域智能体工作流;
- 状态设计;
- 图节点和路由;
- 防护逻辑;
- 评估用例;
- LLM-judge 解析;
- GitHub token 集成;
- Dockerfile 和 Docker Compose 部署;
- 容器故障排除和 CPU/GPU 部署规划。
## 结论
该项目展示了基于 notebook 的 LangGraph 智能体如何结合公开技术来源、应用安全检查、生成有依据的响应、评估自身输出,并在容器化环境中可重现地运行。
Docker 部署保持了原始 notebook 工作流的完整,同时将 Jupyter、Ollama、模型下载、网络和持久化分离为清晰的职责。
标签:AI风险缓解, DLL 劫持, Docker, LLM评估, Ollama, Python, RAG, 人工智能, 大语言模型, 安全防御评估, 无后门, 版权保护, 用户模式Hook绕过, 请求拦截, 逆向工具