# Webwatcher Vision Research

> Develops multimodal research agents that combine visual and textual reasoning for complex information-seeking tasks, using synthetic training trajectories and reinforcement learning.

- Skill: `adu2021/webwatcher-vision-research` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adu2021/webwatcher-vision-research`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adu2021/webwatcher-vision-research/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Research & Search
- License: MIT
- Author: adu2021 (https://skillmd.com/u/adu2021)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/adu2021/webwatcher-vision-research

---


## WebWatcher: Vision-Language Deep Research Agent

### Core Concept

WebWatcher addresses the limitation that most information retrieval remains text-centric by creating agents capable of reasoning across both visual and textual data during complex research tasks. The approach uses high-quality synthetic multimodal trajectories for efficient training and reinforcement learning refinement to develop sophisticated search strategies.

### Architecture Overview

- **Multimodal Input Processing**: Handle both text and images from web search results
- **Synthetic Training Data**: High-quality curated examples of visual-language reasoning
- **Tool Integration Layer**: Access to search, webpage reading, and image analysis tools
- **Reinforcement Learning**: Refine agent strategy through reward signals
- **BrowseComp-VL Benchmark**: Evaluation on multimodal information-seeking tasks

### Implementation Steps

**Step 1: Design Multimodal Perception Module**

Process visual and textual information:

```python
# Pseudocode for multimodal perception
class MultimodalPerceptionModule(nn.Module):
    def __init__(self, vision_model, language_model):
        super().__init__()
        self.vision_model = vision_model  # CLIP or similar
        self.language_model = language_model
        self.fusion_layer = nn.Linear(768 + 768, 768)

    def process_search_result(self, text_content, image_data):
        """
        Process both text and image from search result.

        Args:
            text_content: Text snippet from search result
            image_data: Image from search result or webpage

        Returns:
            fused_representation: Combined multimodal representation
        """
        # Process text
        text_embeddings = self.language_model.encode(text_content)

        # Process image
        image_embeddings = self.vision_model.encode(image_data)

        # Fuse modalities
        combined = torch.cat([text_embeddings, image_embeddings], dim=-1)
        fused = self.fusion_layer(combined)

        return fused

    def extract_visual_information(self, image, question):
        """
        Extract visual features relevant to question.
        """
        # Analyze image for question-relevant details
        image_embedding = self.vision_model.encode(image)

        # Cross-modal attention: question-image alignment
        question_embedding = self.language_model.encode(question)

        # Compute relevance
        relevance_score = torch.cosine_similarity(
            image_embedding,
            question_embedding,
            dim=-1
        )

        return {
            'embedding': image_embedding,
            'relevance': relevance_score,
            'description': self._describe_image(image, question)
        }

    def _describe_image(self, image, question):
        """
        Generate textual description of image.
        """
        # Use vision-language model to describe image
        # considering the question context
        description = self.vision_model.describe(image, context=question)
        return description
```

**Step 2: Create Synthetic Training Trajectory Generator**

Generate high-quality training examples:

```python
# Pseudocode for synthetic trajectory generation
class SyntheticTrajectoryGenerator:
    def __init__(self, search_api, vision_analyzer):
        super().__init__()
        self.search_api = search_api
        self.vision_analyzer = vision_analyzer

    def generate_training_trajectory(self, query, target_answer):
        """
        Generate complete research trajectory for training.

        Args:
            query: Research question
            target_answer: Known correct answer

        Returns:
            trajectory: Sequence of actions and observations
        """
        trajectory = {
            'query': query,
            'steps': [],
            'target_answer': target_answer,
            'reasoning_chain': []
        }

        # Initial search
        search_results = self.search_api.search(query)

        for result in search_results[:5]:
            step = {
                'action': 'search_result_analysis',
                'input': result,
                'reasoning': self._generate_reasoning(result, query)
            }

            # Extract visual and textual information
            if 'image' in result:
                visual_info = self.vision_analyzer.extract_visual_information(
                    result['image'],
                    query
                )
                step['visual_analysis'] = visual_info

            if 'text' in result:
                step['text_analysis'] = self._analyze_text(result['text'], query)

            # Determine if this step helps answer the question
            step['relevance_to_answer'] = self._assess_relevance(
                step,
                target_answer
            )

            trajectory['steps'].append(step)
            trajectory['reasoning_chain'].append(step['reasoning'])

        # Generate final answer
        trajectory['generated_answer'] = self._synthesize_answer(
            trajectory['steps'],
            query
        )

        # Verify correctness
        trajectory['correctness'] = self._verify_answer(
            trajectory['generated_answer'],
            target_answer
        )

        return trajectory

    def _generate_reasoning(self, result, query):
        """
        Generate reasoning about why this result matters.
        """
        reasoning = f"Found result related to '{query}':"
        if 'title' in result:
            reasoning += f" '{result['title']}'"
        if 'snippet' in result:
            reasoning += f" - {result['snippet'][:100]}..."

        return reasoning

    def _analyze_text(self, text, query):
        """
        Analyze text for query-relevant information.
        """
        # Extract key phrases, entities, facts
        key_phrases = self._extract_key_phrases(text, query)
        entities = self._extract_entities(text)

        return {
            'key_phrases': key_phrases,
            'entities': entities,
            'length': len(text),
            'relevance_keywords': self._find_overlapping_keywords(text, query)
        }

    def _assess_relevance(self, step, target_answer):
        """
        Determine if step contributes to answer.
        """
        # Check if step contains answer-relevant information
        step_text = str(step)
        answer_text = str(target_answer)

        shared_tokens = len(set(step_text.split()) & set(answer_text.split()))
        relevance = shared_tokens / (len(set(answer_text.split())) + 1e-8)

        return relevance

    def _synthesize_answer(self, steps, query):
        """
        Synthesize answer from research steps.
        """
        # Combine information from steps
        relevant_steps = [s for s in steps if s['relevance_to_answer'] > 0.1]

        answer_parts = []
        for step in relevant_steps:
            if 'text_analysis' in step:
                answer_parts.append(step['text_analysis']['key_phrases'])

        combined_answer = ' '.join(answer_parts)
        return combined_answer[:200]  # Limit length

    def _verify_answer(self, generated, target):
        """
        Check if generated answer matches target.
        """
        overlap = len(set(generated.split()) & set(target.split()))
        return overlap / (len(set(target.split())) + 1e-8)
```

**Step 3: Implement Tool-Use Interface**

Define agent tools and action space:

```python
# Pseudocode for tool interface
class AgentToolkit:
    def __init__(self, search_engine, webpage_loader, image_analyzer):
        super().__init__()
        self.search = search_engine
        self.load_page = webpage_loader
        self.analyze_image = image_analyzer

    def execute_action(self, action_type, action_params):
        """
        Execute agent action and return observation.

        Args:
            action_type: Type of action (search, load_page, analyze, etc)
            action_params: Parameters for the action

        Returns:
            observation: Result of action
        """
        if action_type == 'search':
            return self.execute_search(action_params['query'])
        elif action_type == 'load_webpage':
            return self.execute_load_page(action_params['url'])
        elif action_type == 'analyze_image':
            return self.execute_analyze_image(
                action_params['image'],
                action_params.get('context')
            )
        elif action_type == 'synthesize':
            return self.execute_synthesize(action_params['information'])
        else:
            return {'error': f'Unknown action: {action_type}'}

    def execute_search(self, query):
        """
        Execute web search.
        """
        results = self.search.search(query)
        return {
            'action': 'search',
            'query': query,
            'results': results[:10],
            'num_results': len(results)
        }

    def execute_load_page(self, url):
        """
        Load and extract information from webpage.
        """
        try:
            page_data = self.load_page.fetch(url)
            return {
                'action': 'load_page',
                'url': url,
                'title': page_data.get('title'),
                'text': page_data.get('text')[:1000],
                'images': page_data.get('images', [])[:3]
            }
        except Exception as e:
            return {'action': 'load_page', 'error': str(e)}

    def execute_analyze_image(self, image, context=None):
        """
        Analyze image for relevant information.
        """
        description = self.analyze_image.describe(image, context=context)
        return {
            'action': 'analyze_image',
            'description': description,
            'confidence': 0.85
        }

    def execute_synthesize(self, information):
        """
        Synthesize final answer from collected information.
        """
        # Combine all information
        answer = ' '.join([
            info['text'] if isinstance(info, dict) else str(info)
            for info in information
        ])
        return {
            'action': 'synthesize',
            'answer': answer[:500],
            'sources': len(information)
        }
```

**Step 4: Implement Reinforcement Learning Training**

Optimize agent policy:

```python
# Pseudocode for RL training
class WebWatcherRLTrainer:
    def __init__(self, agent, toolkit):
        super().__init__()
        self.agent = agent
        self.toolkit = toolkit

    def compute_trajectory_reward(self, trajectory):
        """
        Compute reward for complete research trajectory.

        Args:
            trajectory: Generated research trajectory

        Returns:
            reward: Scalar reward value
        """
        # Reward components
        correctness_reward = trajectory['correctness']  # 0-1

        # Efficiency reward: fewer steps is better
        num_steps = len(trajectory['steps'])
        efficiency_reward = 1.0 / (1.0 + num_steps / 5.0)  # Prefer <=5 steps

        # Diversity reward: use both visual and textual info
        visual_steps = sum(1 for s in trajectory['steps'] if 'visual_analysis' in s)
        diversity_reward = min(visual_steps / 3.0, 1.0)

        # Combined reward
        total_reward = (
            0.6 * correctness_reward +
            0.2 * efficiency_reward +
            0.2 * diversity_reward
        )

        return total_reward

    def train_agent(self, synthetic_trajectories, num_epochs=3):
        """
        Train agent on synthetic trajectories with RL.
        """
        optimizer = AdamW(self.agent.parameters(), lr=1e-4)

        for epoch in range(num_epochs):
            epoch_loss = 0

            for trajectory in synthetic_trajectories:
                # Compute reward
                reward = self.compute_trajectory_reward(trajectory)

                # Forward pass through agent
                states = trajectory['steps']
                actions_taken = []

                for state in states:
                    action_logits = self.agent.choose_action(state)
                    action = self._sample_action(action_logits)
                    actions_taken.append(action)

                # Compute policy loss
                log_probs = self._compute_log_probs(actions_taken, trajectory)
                loss = -log_probs.mean() * reward

                # Backward pass
                optimizer.zero_grad()
                loss.backward()
                torch.nn.utils.clip_grad_norm_(self.agent.parameters(), 1.0)
                optimizer.step()

                epoch_loss += loss.item()

            print(f"Epoch {epoch+1}: Loss = {epoch_loss / len(synthetic_trajectories):.4f}")

        return self.agent

    def _sample_action(self, logits):
        """
        Sample action from policy logits.
        """
        probs = F.softmax(logits, dim=-1)
        action = torch.multinomial(probs, 1)
        return action

    def _compute_log_probs(self, actions, trajectory):
        """
        Compute log probabilities for taken actions.
        """
        # Sum log probs across trajectory
        return torch.tensor([0.1 * len(actions)])  # Simplified
```

### Practical Guidance

**Hyperparameters and Configuration**:
- Maximum search depth: 5-10 steps
- Number of search results to consider: 5-20
- RL learning rate: 1e-4 to 5e-5
- Reward weights: 60% correctness, 20% efficiency, 20% diversity
- Training epochs: 3-5 on synthetic data

**When to Use WebWatcher**:
- Complex information-seeking tasks requiring both visual and textual analysis
- Research applications where images contain critical information
- Scenarios with diverse web sources (text, images, tables)
- Systems where reasoning transparency is valued

**When NOT to Use**:
- Simple factual lookup tasks (single source sufficient)
- Text-only information retrieval (images not helpful)
- Scenarios with no visual information in sources
- Real-time systems with strict latency constraints

**Implementation Notes**:
- Synthetic trajectories should be high quality (manually validated)
- Visual information can disambiguate text-only queries
- RL training stabilizes policy through diverse examples
- Consider domain-specific vision models for specialized images
- Monitor that agent doesn't over-rely on single modality

### Reference

Paper: WebWatcher: Vision-Language Deep Research Agent
ArXiv: 2508.05748
Performance: Significantly outperforms proprietary baselines, RAG workflows, and open-source agents on four challenging VQA benchmarks via multimodal reasoning

